Android实训案例(四)——关于Game,2048方块的设计,逻辑,实现,编写,加上色彩,分数等深度剖析开发过程!

简介: <div class="markdown_views"><h1 id="android实训案例四关于game2048方块的设计逻辑实现编写加上色彩分数等深度剖析开发过程">Android实训案例(四)——关于Game,2048方块的设计,逻辑,实现,编写,加上色彩,分数等深度剖析开发过程!</h1><hr><blockquote> <p>关于2048,我看到很多大神

Android实训案例(四)——关于Game,2048方块的设计,逻辑,实现,编写,加上色彩,分数等深度剖析开发过程!


关于2048,我看到很多大神,比如医生,郭神,所以我也研究了一段时间,还好是研究了一套逻辑,这是一整套的2048游戏从设计到逻辑再到编写的全部过程,小伙伴们看仔细咯,刚好今天是礼拜天,一天应该了一把这篇博客发表了,其实2048开发起来还是有点难度的,并且他的逻辑挺强的,我也是看了很多的资料偷学的,很适合来锻炼自己的逻辑性

我们首先先来选择开发环境,这里我们就以Eclipse为IDE,新建一个工程——Game2048

这里写图片描述

一.Score分数

既然是2048游戏,我们也就做一个简单的,他有一个分数,然后就是一个游戏的布局,我们也做一个简单的4*4的游戏,大概的设计图就是这样

这里写图片描述

二.游戏类:GameView

因为我们的游戏所使用到的布局就是GridLayout,所以我们新建一个GameView继承自GridLayout,然后通过算法动态添加方块,并且监听手势进行操作,这个重写的GridLayout就是游戏的布局了

<com.lgl.game2048.GameView
        android:id="@+id/game_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

三.实现交互逻辑

我们这里铁定是手势操作啦,这里我们就得区分手势是往上,往下,往左,往右,的手势,这时候,我们就需要用到手势监听——OnTouchListener了,也为了确保是实时监听,我们直接在initView的初始方法中处理
我们其实只要知道两点,用户手指按下的坐标点和手指离开的坐标点,然后进行比对,就能识别出用户的意图了

其实关于MotionEvent的几个方法,大家估计都见怪不怪了,因为用的太多了
// 初始化
    private void initView() {
        // 识别手势
        setOnTouchListener(new OnTouchListener() {

            // 起始点和偏移点
            private float startX, startY, offsetX, offsetY;

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                /**
                 * 交互逻辑 :我们其实只要知道两点,用户手指按下的坐标点和手指离开的坐标点,然后进行比对,就能识别出用户的意图了
                 */
                switch (event.getAction()) {
                // 手指按下
                case MotionEvent.ACTION_DOWN:
                    // 记录按下的x,y坐标
                    startX = event.getX();
                    startY = event.getY();

                    break;
                // 手指离开
                case MotionEvent.ACTION_UP:
                    // 手指离开之后计算偏移量(离开的位置-按下的位置在进行判断是往哪个方向移动)
                    offsetX = event.getX() - startX;
                    offsetY = event.getY() - startY;

                    // 开始识别方向
                    // offsetX 的绝对值大于offsetY的绝对值 说明在水平方向
                    if (Math.abs(offsetX) > Math.abs(offsetY)) {
                        // (直接<0 会有些许误差,我们可以 <-5)
                        if (offsetX < -5) {
                            // 左
                            System.out.println("左");
                        } else if (offsetX > 5) {
                            // 右
                            System.out.println("右");
                        }
                        // 开始计算垂直方向上下的滑动
                    } else {
                        if (offsetY < -5) {
                            // 上
                            System.out.println("上");
                        } else if (offsetY > 5) {
                            // 下
                            System.out.println("下");
                        }
                    }
                    break;
                }
                return true;
            }
        });
    }
上面的逻辑是不是非常的简单,然后我们操作一下,看log

这里写图片描述

现在手势识别也是很精准了,当然,我们的代码设计也不能太过臃肿,所以,我们的操作逻辑就不在里面编写了,我们分别实现四个方向的方法

    // 左
    private void isLeft() {

    }

    // 右
    private void isRight() {

    }

    // 上
    private void isTop() {

    }

    // 下
    private void isButtom() {

    }
然后把输出语句替换掉,监听到哪个方向就执行哪个方法
//System.out.println("上,下,左,右");
 private void isXXX() {

    }

四.实现方块类CardView

我们可以把这一个个卡片看作是一个对象,我们每次操作,他都要进行实例化
首先,我们新建一个类CardView继承自FrameLayout,再里面我们要考虑三点

1.卡片
2.卡片上的数字
卡片相同的比较

package com.lgl.game2048;

import android.content.Context;
import android.widget.FrameLayout;
import android.widget.TextView;

public class CardView extends FrameLayout {

    // 卡片数量
    private int num = 0;
    // 卡片文字
    private TextView tv_num;

    public CardView(Context context) {
        super(context);
        // 初始化TextView
        tv_num = new TextView(getContext());
        // 卡片文字大小
        tv_num.setTextSize(20);
        // 布局控制器,填充满整个父容器
        LayoutParams lp = new LayoutParams(-1, -1);
        addView(tv_num, lp);

        setNum(0);
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;

        // 要呈现出来的文字(这里要注意是String类型的)
        tv_num.setText(num + "");
    }

    // 两卡片相同的比较方法
    public boolean equals(CardView card) {

        return getNum() == card.getNum();
    }

}

五.动态分配方块的宽高以及添加方块

1.动态分配方块的宽高

写到这里,就有一个梗了,还是Android的老毛病,屏幕的适配问题,所以我们队卡牌的宽高是不能做限定的,也就是说我们要去根据手机屏幕动态分配卡片的width和height,在这里我们就得用到我之前一篇博客
Android绘图机制(一)——自定义View的基础属性和方法
中提到的一个方法了

@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        // TODO Auto-generated method stub
        super.onSizeChanged(w, h, oldw, oldh);
    }

他负责我们的View在父容器的位置,所以我们动态分配高宽也是在他这里面完成,首先,位置发生改变之后,我们得到的位置是一个确定数,但是为了防止用户是横放这手机,这就蛋疼了,所以我们得进行一个设置了
我们打开AndroidManifest.xml的activity标签中加入

//禁止屏幕横屏
android:screenOrientation="portrait"    

好的,现在可以计算了
宽高求最小值 因为考虑到,我们的方阵他是正方形的,而手机屏幕是长方形的,这样,我们的正方形要设置变长就得求长方形的宽,也就是最小值了
并且我们也不需要他填满宽度,我们需要一点空隙,所以我们减去10个像素

这里写图片描述

再让他除以4,通过这种方式,我们就可以动态平分这个宽度了
int cardWidth = (Math.min(w, h)-10)/4;

2.添加方块

-1.添加卡片

    // 添加卡片,参数为卡片的宽高,因为他是正方形,所以宽高都是cardWidth
    private void addCard(int cardWidth, int cardHeight) {
        // 创建方块
        CardView c;
        // 循环添加
        for (int i = 0; i < 4; i++) {
            for (int j = 0; j < 4; j++) {
                c = new CardView(getContext());
                // num为随机数
                c.setNum(2);
                addView(c, cardWidth, cardHeight);
            }
        }
    }
好的,我们来运行一下(换个AS2.0的模拟器感觉萌萌哒)

这里写图片描述

-2.方块换行

你会发现,并没有4*4,而且一排没有换行,我们回到GameView的initView()方法中添加
//换行
setColumnCount(4);
现在再看看

这里写图片描述

-3.文字居中

现在我们调整一下,让文字居中,在CardView中
    //文字居中
    tv_num.setGravity(Gravity.CENTER);

这里写图片描述

现在是不是好看多了

-4.方块颜色

既然是玩2048我们怎么能少了颜色尼,我们就根据这

Android高效率编码-细节,控件,架包,功能,工具,开源汇总
color
中的色彩表来,自己觉得什么颜色好看可以自行替换,我们直接来到CardView里面

//设置文字背景(暗卡其色)
tv_num.setBackgroundColor(0xffBDB76A);

这里写图片描述

-5.方块间距

既然是方块间隔,我们还是回到CardView里面,还记得我们设置的LayoutParams吗?你可能想到了吧,我们用Margins
//设置间距
lp.setMargins(10, 10, 0, 0);

这里写图片描述

-6.记忆方块

我们所操作之后,会有新生成的卡片,为了不重合,我们得做一个记忆功能
    // 记录卡片的二维数组
     private CardView[][] cards = new CardView[4][4];
然后在addCard()方法中
//记忆
    cards[j][i] = c;

六.随机数

我们先来思考一下这个随机数的逻辑,我们玩2048的时候,是不是开始新游戏的时候会随机出现两个方块,而这两个方块,他是随机出现在4*4的任意位置的,所以,我们确定下来,一开始是两个方块的随机出现,再接下来,我们会发现,他有时候是两个2,但是有时候是一个2,一个4,这个4出现的几率有点小,而且我们作为游戏规则制定者,这个也是我们控制的,这里,我不想他出现的很容易,所以我这里的逻辑就设置成1-9,这样4出现的概率会小很多,好了,基本确定了,我们就开始写代码了,我们写一个方法,在此之前,我们要对之前的代码进行调整一下,在CardView中setNum方法中,我们默认为0就占一格,

// 要呈现出来的文字(这里要注意是String类型的)
        if (num <= 0) {
            tv_num.setText("");
        } else {
            tv_num.setText(num + "");
        }
然后把刚才的文字设置换成0
// c.setNum(2);
   c.setNum(0);
然后我们就可以添加随机数了,我们新建一个方法addRandom();
    // 随机数
    private void addRandom() {
        // 我们新建一个lsit存放空的方块,操作之前清空
        point.clear();
        // 对所有的位置进行遍历
        for (int i = 0; i < 4; i++) {
            for (int j = 0; j < 4; j++) {
                // 空方块才可以添加数字,有值我们就不添加
                if (cards[j][i].getNum() <= 0) {
                    point.add(new Point(j, i));
                }
            }
        }
        // for循环走完之后我们要取方块
        Point p = point.remove((int) (Math.random() * point.size()));
        // 我们用Math.random()返回一个0-1的数,当大于0.1的时候是2否则就是4,也就是4出现的概率为十分之一
        cards[p.x][p.y].setNum(Math.random() > 0.1 ? 2 : 4);
    }
这个时候我们就可以开始游戏了,为了方便等下我们需要重新开始游戏,我们就新建一个startGame()方法,让他在onSizeChanged()调用
// 开启游戏
    private void startGame() {
        // 既然是开始游戏,我们就要对所有的值进行清理
        for (int i = 0; i < 4; i++) {
            for (int j = 0; j < 4; j++) {
                cards[j][i].setNum(0);
            }
        }
        // 重新添加随机数
        addRandom();
        // 我们要添加两个
        addRandom();
    }
好的,我们现在运行一下啊

这里写图片描述

这正是我们要的效果,每次进入游戏随机生成两个方块,他会出现在不同的位置,而且会出现2和4,4的概率小很多

七.实现方块滑动,递加逻辑

好的,终于到了核心的东西了,这次我们就要用到之前所写的上下左右方向方法了
// 左
    private void isLeft() {
        /**
         * 这里的逻辑有三种情况 1.左边为空,直接左滑到最后一格 2.左边碰到的第一个数是相等的,就相加 3.左边碰到的第一个数是不相等的,靠在旁边
         */
        for (int i = 0; i < 4; i++) {
            for (int j = 0; j < 4; j++) {
                // 往左滑是一行一行去遍历的
                for (int j2 = j + 1; j2 < 4; j2++) {
                    // 如果说遍历到值
                    if (cards[j2][i].getNum() > 0) {
                        // 如果当前位置上为0,就放在这个位置上去
                        if (cards[j][i].getNum() <= 0) {
                            cards[j][i].setNum(cards[j2][i].getNum());
                            // 把原来位置上的数字清除
                            cards[j2][i].setNum(0);
                            // 让图形继续遍历
                            j--;

                            // 有值,并且还相同
                        } else if (cards[j][i].equals(cards[j2][i])) {
                            // 合并,这里做了一个很巧妙的写法,我们相加,其实2048方块上的数字都是双倍的,所以我们只要原数据*2就可以了
                            cards[j][i].setNum(cards[j][i].getNum() * 2);
                            // 把原来位置上的数字清除
                            cards[j2][i].setNum(0);

                        }
                        break;
                    }
                }
            }
        }
    }

    // 右
    private void isRight() {
        /**
         * 这里的逻辑有三种情况 1.左边为空,直接左滑到最后一格 2.左边碰到的第一个数是相等的,就相加 3.左边碰到的第一个数是不相等的,靠在旁边
         */
        for (int i = 0; i < 4; i++) {
            for (int j = 3; j >= 0; j--) {
                // 往左滑是一行一行去遍历的
                for (int j2 = j - 1; j2 >= 0; j2--) {
                    // 如果说遍历到值
                    if (cards[j2][i].getNum() > 0) {
                        // 如果当前位置上为0,就放在这个位置上去
                        if (cards[j][i].getNum() <= 0) {
                            cards[j][i].setNum(cards[j2][i].getNum());
                            // 把原来位置上的数字清除
                            cards[j2][i].setNum(0);
                            // 让图形继续遍历
                            j++;

                            // 有值,并且还相同
                        } else if (cards[j][i].equals(cards[j2][i])) {
                            // 合并,这里做了一个很巧妙的写法,我们相加,其实2048方块上的数字都是双倍的,所以我们只要原数据*2就可以了
                            cards[j][i].setNum(cards[j][i].getNum() * 2);
                            // 把原来位置上的数字清除
                            cards[j2][i].setNum(0);

                        }
                        break;
                    }
                }
            }
        }
    }

    // 上
    private void isTop() {
        /**
         * 这里的逻辑有三种情况 1.左边为空,直接左滑到最后一格 2.左边碰到的第一个数是相等的,就相加 3.左边碰到的第一个数是不相等的,靠在旁边
         */
        for (int j = 0; j < 4; j++) {
            for (int i = 0; i < 4; i++) {
                // 往左滑是一行一行去遍历的
                for (int i2 = i + 1; i2 < 4; i2++) {
                    // 如果说遍历到值
                    if (cards[j][i2].getNum() > 0) {
                        // 如果当前位置上为0,就放在这个位置上去
                        if (cards[j][i].getNum() <= 0) {
                            cards[j][i].setNum(cards[j][i2].getNum());
                            // 把原来位置上的数字清除
                            cards[j][i2].setNum(0);
                            // 让图形继续遍历
                            i--;

                            // 有值,并且还相同
                        } else if (cards[j][i].equals(cards[j][i2])) {
                            // 合并,这里做了一个很巧妙的写法,我们相加,其实2048方块上的数字都是双倍的,所以我们只要原数据*2就可以了
                            cards[j][i].setNum(cards[j][i].getNum() * 2);
                            // 把原来位置上的数字清除
                            cards[j][i2].setNum(0);

                        }
                        break;
                    }
                }
            }
        }
    }

    // 下
    private void isButtom() {
        /**
         * 这里的逻辑有三种情况 1.左边为空,直接左滑到最后一格 2.左边碰到的第一个数是相等的,就相加 3.左边碰到的第一个数是不相等的,靠在旁边
         */
        for (int j = 0; j < 4; j++) {
            for (int i = 3; i >= 0; i--) {
                // 往左滑是一行一行去遍历的
                for (int i2 = i - 1; i2 >= 0; i2--) {
                    // 如果说遍历到值
                    if (cards[j][i2].getNum() > 0) {
                        // 如果当前位置上为0,就放在这个位置上去
                        if (cards[j][i].getNum() <= 0) {
                            cards[j][i].setNum(cards[j][i2].getNum());
                            // 把原来位置上的数字清除
                            cards[j][i2].setNum(0);
                            // 让图形继续遍历
                            i++;

                            // 有值,并且还相同
                        } else if (cards[j][i].equals(cards[j][i2])) {
                            // 合并,这里做了一个很巧妙的写法,我们相加,其实2048方块上的数字都是双倍的,所以我们只要原数据*2就可以了
                            cards[j][i].setNum(cards[j][i].getNum() * 2);
                            // 把原来位置上的数字清除
                            cards[j][i2].setNum(0);

                        }
                        break;
                    }
                }
            }
        }
    }
四个方法的逻辑都是大同小异的,不过逻辑性还是很强的,大家可以适当的去研究一下然后我们多增加几个方块先来模拟下效果

这里写图片描述

八.计分

方块的逻辑差不多写完了,我们先来就在MainActivity里面来实现我们的Score计分

MainActivity

package com.lgl.game2048;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends Activity {

    private TextView tv_score;
    //外界可以访问的实例
    private static MainActivity mainActivity = null;
    //积分器
    private int score = 0;

    public MainActivity() {
        mainActivity = this;
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tv_score = (TextView) findViewById(R.id.tv_score);

    }
    public static MainActivity getMainActivity() {
        return mainActivity;
    }
    public static void setMainActivity(MainActivity mainActivity) {
        MainActivity.mainActivity = mainActivity;
    }

    //清除分数
    public void clearScore(){
        score = 0;
        showScore();
    }
    //分数
    public void showScore(){
        tv_score.setText("分数:"+score);
    }

    public void addScore(int s){
        score += s;
        showScore();
    }
}
这是我们计分的过程,我们思考一下在什么时候计分呢?想想就知道在滑动的时候俩值相加的时候开始计分,所以我们在四个滑动方法有值的判断句中添加
//开始计分                                     MainActivity.getMainActivity().addScore(cards[j][i].getNum());
同时,我们在开始游戏的时候要清零,所以我们在startGame方法中要添加
//计分清零
MainActivity.getMainActivity().clearScore();

九.滑动后增加方块

我们默认进来是两个方块,但是滑动之后我们应该也要随机增加方块才能达到游戏的逻辑,你说是吧!
所以,只要你滑动了,我们就要添加,一直到gameover结束为止,那我们依然在那四个方向方法里写
private void isxx(){
    // 加个判断是否可以添加
    boolean isAdd = false;
    for(....){
        for(....){
            for(....){
                if(....){
                    if(....){
                        ....
                    // 可以添加
                    isAdd = true;
                    }else if(....){
                        ....
                        // 可以添加
                        isAdd = true;
                    }
                }
            }
        }
    }
    // 开始进行判断
        if (isAdd) {
            // 如果可以合并,我们添加随机数
            addRandom();
        }
}
好的,我们现在来运行一下

这里写图片描述

游戏现在大体上是OK的了

十.游戏结束

游戏有始有终,我们现在就来判断游戏结束,游戏结束有两个前提
1.16个格子都是满的
2.上下左右相邻的格子都不相同
这样的话我们就可以写一个endGame方法,然后让他在每次增加方块的时候调用了

    // 游戏结束
    private void endGame() {
        // 在每次添加新的方块的时候判断一下
        // 是否结束?
        boolean isEnd = true;
        ALL: // 标签,让break跳出整个循环
        for (int i = 0; i < 4; i++) {
            for (int j = 0; j < 4; j++) {
                // 等于0的话游戏没有结束,或者上下左右还是有相同的数
                if (cards[j][i].getNum() == 0
                // 左
                        || (j > 0 && cards[j][i].equals(cards[j - 1][i]))
                        // 右
                        || (j < 3) && cards[j][i].equals(cards[j + 1][i])
                        // 上
                        || (i > 0 && cards[j][i].equals(cards[j][i - 1]))
                        // 下
                        || (i < 3 && cards[j][i].equals(cards[j][i + 1]))) {
                    // 说明游戏没有结束
                    isEnd = false;
                    break ALL;

                }
            }
        }
        if (isEnd) {
            // 当isEnd = true的时候游戏结束
            new AlertDialog.Builder(getContext())
                    .setTitle("Sorry,游戏结束!")
                    .setMessage("是否重新开始?")
                    .setPositiveButton("是",
                            new DialogInterface.OnClickListener() {

                                @Override
                                public void onClick(DialogInterface dialog,
                                        int which) {
                                    // 重新开始
                                    startGame();
                                }
                            })
                    .setNegativeButton("否",
                            new DialogInterface.OnClickListener() {

                                @Override
                                public void onClick(DialogInterface dialog,
                                        int which) {
                                }
                            }).show();
        }
    }
好的,现在我们可以来检测一下了

这里写图片描述

十一.优化之色块

我们的数字每个数字代表一种颜色,这里我就简单的写点颜色,你们要是喜欢可以自己想改什么就改什么
在CardView中setnum方法里
        switch (num) {
        case 0:
            tv_num.setBackgroundColor(0xffBDB76A);
            break;
        case 2:
            tv_num.setBackgroundColor(0xffeee4da);
            break;
        case 4:
            tv_num.setBackgroundColor(0xffede0c8);
            break;
        case 8:
            tv_num.setBackgroundColor(0xfff2b179);
            break;
        case 16:
            tv_num.setBackgroundColor(0xfff59563);
            break;
        case 32:
            tv_num.setBackgroundColor(0xfff67c5f);
            break;
        case 64:
            tv_num.setBackgroundColor(0xfff65e3b);
            break;
        case 128:
            tv_num.setBackgroundColor(0xffedcf72);
            break;
        case 256:
            tv_num.setBackgroundColor(0xffedcc61);
            break;
        case 512:
            tv_num.setBackgroundColor(0xffedc850);
            break;
        case 1024:
            tv_num.setBackgroundColor(0xffedc53f);
            break;
        case 2048:
            tv_num.setBackgroundColor(0xffedc22e);
            break;
        default:
            tv_num.setBackgroundColor(0xff3c3a32);
            break;
        }
我们运行下

这里写图片描述

Demo下载地址:http://download.csdn.net/detail/qq_26787115/9409068

目录
相关文章
|
3月前
|
监控 Android开发 C语言
深度解读Android崩溃日志案例分析2:tombstone日志
深度解读Android崩溃日志案例分析2:tombstone日志
84 0
|
6月前
|
数据库连接 数据库 Android开发
Android -- Room简化数据库设置图书案例
Android -- Room简化数据库设置图书案例
22 0
|
1天前
android-agent-web中js-bridge案例
android-agent-web中js-bridge案例
9 2
|
5月前
|
编译器 Android开发
深度解读Android崩溃日志案例分析1:so崩溃
深度解读Android崩溃日志案例分析1:so崩溃
97 1
|
6月前
|
Android开发
Android 小案例 -- 计算器
摘要: 计算器的界面分为两大部分,第一部分是上方的计算表达式,既包括用户的按键输入,也包括计算结果数字;第二部分是下方的各个按键,例如:从0到9的数字按钮、加减乘除与等号、正负号按钮、小数点按钮、求倒数按钮、开方按钮以及删除、清空、取消等控制按钮
49 0
|
6月前
|
测试技术 开发工具 数据库
《移动互联网技术》第十一章 Android应用工程案例: 掌握Android系统的需求分析和设计以及 Android项目的程序测试和版本管理方法
《移动互联网技术》第十一章 Android应用工程案例: 掌握Android系统的需求分析和设计以及 Android项目的程序测试和版本管理方法
71 0
|
8月前
|
Java 开发工具 Android开发
逻辑清晰,详解社交源码Android开发SDK
前篇我们讲解了有关如何在IOS平台开发集成SDK,那么今天来给大家简单讲解下如何在社交源码Android客户端上开发集成 SDK。
逻辑清晰,详解社交源码Android开发SDK
|
9月前
|
存储 前端开发 Java
现代化 Android 开发:逻辑层
本文为现代化 Android 开发系列文章第三篇。
88 0
|
3天前
|
Linux 编译器 Android开发
FFmpeg开发笔记(九)Linux交叉编译Android的x265库
在Linux环境下,本文指导如何交叉编译x265的so库以适应Android。首先,需安装cmake和下载android-ndk-r21e。接着,下载x265源码,修改crosscompile.cmake的编译器设置。配置x265源码,使用指定的NDK路径,并在配置界面修改相关选项。随后,修改编译规则,编译并安装x265,调整pc描述文件并更新PKG_CONFIG_PATH。最后,修改FFmpeg配置脚本启用x265支持,编译安装FFmpeg,将生成的so文件导入Android工程,调整gradle配置以确保顺利运行。
24 1
FFmpeg开发笔记(九)Linux交叉编译Android的x265库
|
26天前
|
Java Android开发
Android 开发获取通知栏权限时会出现两个应用图标
Android 开发获取通知栏权限时会出现两个应用图标
12 0