• 安卓2048源码分析


    2048游戏最近很火,想看下源码,却不会JavaScript。网上搜了搜安卓版的源码,尝试下来学习。

     uberspot

    在https://github.com/uberspot/2048-android 上面发现了一个安卓版的2048代码,于是下载准备阅读。却发现源文件中只有一个Java类,MainActivity.java。打开大致看了一下:

    // If there is a previous instance restore it in the webview
    if (savedInstanceState != null) {
        mWebView.restoreState(savedInstanceState);
    } else {
        mWebView.loadUrl("file:///android_asset/2048/index.html");
    }

    原来是用一个webview对原本的JavaScript进行了封装,使用安卓内部webkit浏览器进行了加载。相当于用手机浏览器玩网页版的游戏,只能再搜索了。

    极客学院

    另外找到一个极客学院版本的源码,在网站上面还有视频教程。本文分析的主要内容就是极客学院版本的源代码了,作者是ime。

    源码链接:https://github.com/plter/Android2048GameLesson

    分析的目标为codeideADTGame2048Publish目录中的源码版本

     1 界面

    界面比较简单了,打开activity_main.xml看看,几个TextView,一个按钮,还有三个自定义的控件GameView,AnimLayer,Card。游戏的截图如下:

    image

    标准控件就不介绍了,介绍一下系统三个自定义的控件。

     1.1 Card(后文混用Card 卡片 方块三个词语)

    类Card继承了FrameLayout,目的是作为游戏中的卡片。卡片数字和样式的实现:

    public void setNum(int num) {
            this.num = num;
    
            if (num<=0) {
                label.setText("");
            }else{
                label.setText(num+"");
            }
    
            switch (num) {
            case 0:
                label.setBackgroundColor(0x00000000);//透明色
                break;
            case 2:
                label.setBackgroundColor(0xffeee4da);
                break;
            case 4:
                label.setBackgroundColor(0xffede0c8);
                break;
            case 8:
                label.setBackgroundColor(0xfff2b179);
                break;
            case 16:
                label.setBackgroundColor(0xfff59563);
                break;
                ……
                 default:
                label.setBackgroundColor(0xff3c3a32);
                break;
            }
    }

    num<=0表明是空白方格。当前位置上如果没有card,则使用num<=0的card进行替代。card 0没有label,同时底色为透明。除了card 0之外,card 2之后的卡片都有对应的颜色和数字。

     1.2 AnimLayer

    类AnimLayer继承了FramLayout,用于动画展示。在极客学院安卓2048最主要由两个动画:卡片移动和卡片出现。

    a) 对于卡片出现动画:

    //目标卡片
    public void createScaleTo1(Card target){
        //缩放
         ScaleAnimation sa = new ScaleAnimation(0.1f, 1, 0.1f, 1, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
        sa.setDuration(100);
        target.setAnimation(null);
        target.getLabel().startAnimation(sa);
       }

    b) 对于卡片移动动画:

    使用ArrayList<Card> cards用于管理临时卡片的创建和回收(避免每次创建临时卡片时创建新的对象)
    创建一个临时卡片,从卡片from移动到卡片to,当完成动画之后将临时卡片设为不可见,并使用cards回收该卡片。
    创建卡片:
    private Card getCard(int num){
        Card c;
        if (cards.size()>0) {
            c = cards.remove(0);
        }else{
            c = new Card(getContext());
            addView(c);
        }
        c.setVisibility(View.VISIBLE);
        c.setNum(num);
        return c;
      }

    创建卡片时,如果cards不为空,则从cards队首取出一张临时卡片。(这里认为使用LinkedList<Card>更加适合临时卡片管理队列)

    回收卡片:

    private void recycleCard(Card c){
        c.setVisibility(View.INVISIBLE);
        c.setAnimation(null);
        cards.add(c);
      }

    回收卡片将当前卡片设为不可见,并加入到cards中。

    卡片移动:

    public void createMoveAnim(final Card from,final Card to,int fromX,int toX,int fromY,int toY){
        //临时卡片
        final Card c = getCard(from.getNum());
    
        //设置布局
        LayoutParams lp = new LayoutParams(Config.CARD_WIDTH, Config.CARD_WIDTH);
        lp.leftMargin = fromX*Config.CARD_WIDTH;
        lp.topMargin = fromY*Config.CARD_WIDTH;
        c.setLayoutParams(lp);
    
        if (to.getNum()<=0) {
            to.getLabel().setVisibility(View.INVISIBLE);
        }
        //从from卡片位置移动到to卡片
        TranslateAnimation ta = new TranslateAnimation(0, Config.CARD_WIDTH*(toX-fromX), 0, Config.CARD_WIDTH*(toY-fromY));
        ta.setDuration(25);
        ta.setAnimationListener(new Animation.AnimationListener() {
    
            @Override
            public void onAnimationStart(Animation animation) {}
    
            @Override
            public void onAnimationRepeat(Animation animation) {}
    
            //动画结束,将临时卡片回收
            @Override
            public void onAnimationEnd(Animation animation) {
                to.getLabel().setVisibility(View.VISIBLE);
                recycleCard(c);
            }
        });
        c.startAnimation(ta);
    
      }

    1.3 GameView

    GameView继承了GridLayout,包含了界面和游戏逻辑两个部分。这里介绍界面。

    界面中比较重要的内容就是手势识别,用于操控格子的移动:

    private void initGameView(){
        setColumnCount(Config.LINES);
        setBackgroundColor(0xffbbada0);
        setOnTouchListener(new View.OnTouchListener() {
    
            private float startX,startY,offsetX,offsetY;
    
            @Override
            public boolean onTouch(View v, MotionEvent event) {
    
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN://按下坐标
                        startX = event.getX();
                        startY = event.getY();
                        break;
                    case MotionEvent.ACTION_UP:
                        offsetX = event.getX()-startX;
                        offsetY = event.getY()-startY;
                        if (Math.abs(offsetX)>Math.abs(offsetY)) {
                            if (offsetX<-5) {
                                swipeLeft();
                            }else if (offsetX>5) {
                                swipeRight();
                            }
                        }else{
                            if (offsetY<-5) {
                                swipeUp();
                            }else if (offsetY>5) {
                                swipeDown();
                            }
                        }
                        break;
                }
                return true;//listener已经处理了事件
            }
        });
         }

    使用了View.OnTouchListener来侦听触摸事件:计算按下和抬起来时offsetX和offsetY,预测手势的移动。

    2 游戏逻辑

    上一节介绍了基本的界面展现,本节介绍业务逻辑,即游戏实现原理。

    2.1 游戏初始化

    调用函数initGameView()完成游戏初始化:

    private void initGameView(){
        setColumnCount(Config.LINES);//设置行数量
        setBackgroundColor(0xffbbada0);
    
    
        setOnTouchListener(new View.OnTouchListener() {
            }
        });
    }

    设置控件的方格数量,随后设置了控件北京,最后注册了刚才分析过的触摸事件监听器。此时游戏已经准备好了,正式开始。

     2.2 开始游戏

    函数startGame();正式开始游戏,首先向方格内随机写入两个方块:

    public void startGame(){
    
        MainActivity aty = MainActivity.getMainActivity();
        aty.clearScore();
        aty.showBestScore(aty.getBestScore());
    
        for (int y = 0; y < Config.LINES; y++) {
            for (int x = 0; x < Config.LINES; x++) {
                cardsMap[x][y].setNum(0);
            }
        }
    
        addRandomNum();
        addRandomNum();
    }

    这个函数addRandomNum()向游戏面板内随机加入两个方块,开始游戏:

    private void addRandomNum(){
        //private List<Point> emptyPoints = new ArrayList<Point>(); 
        emptyPoints.clear();
    
        //将所有空格子搜集起来
        for (int y = 0; y < Config.LINES; y++) {
            for (int x = 0; x < Config.LINES; x++) {
                if (cardsMap[x][y].getNum()<=0) {
                    emptyPoints.add(new Point(x, y));
                }
            }
        }
    
        if (emptyPoints.size()>0) {
            //随机位置生成一个card
            Point p = emptyPoints.remove((int)(Math.random()*emptyPoints.size()));
            int num = Math.random()>0.1?2:4;
            cardsMap[p.x][p.y].setNum(num);
            MainActivity.getMainActivity().getAnimLayer().createScaleTo1(cardsMap[p.x][p.y]);
        }
    }

    函数addRandomNum()向面板中空的格子中随机生成一个卡片。首先搜集面板中所有空的位置,搜集到一个List中,最后生成随机数,随机生成一个数字,并完成生成动画。

     2.3 移动

    2048游戏通过游戏中所有的方格朝某个方向移动,合并相同数字的方块。有四个函数负责移动,分别是上下左右,这里只分析一个方向。

    private void swipeLeft(){
    
        boolean merge = false;//是否合并卡片, 1空卡片和已有卡片合并 2两个数字相同的卡片合并
    
        for (int y = 0; y < Config.LINES; y++) {//对所有列
            for (int x = 0; x < Config.LINES; x++) {
                //检查当前点的右侧是否有非空卡片(非空:num>=2)
                for (int x1 = x+1; x1 < Config.LINES; x1++) {
                    if (cardsMap[x1][y].getNum()>0) {//如果右边有非空卡片
    
                        if (cardsMap[x][y].getNum()<=0) {//当前坐标上没有格子(空卡片和已有卡片合并)
    
                            MainActivity.getMainActivity().getAnimLayer().createMoveAnim(cardsMap[x1][y],cardsMap[x][y], x1, x, y, y);
    
                            cardsMap[x][y].setNum(cardsMap[x1][y].getNum());
                            cardsMap[x1][y].setNum(0);
    
                            x--;//和空卡片合并,还需要从当前位置计算(否则:|0|2|2|2|左移之后变为|2|2|2|0|)
                            merge = true;
    
                        }else if (cardsMap[x][y].equals(cardsMap[x1][y])) {
                            MainActivity.getMainActivity().getAnimLayer().createMoveAnim(cardsMap[x1][y], cardsMap[x][y],x1, x, y, y);
                            cardsMap[x][y].setNum(cardsMap[x][y].getNum()*2);
                            cardsMap[x1][y].setNum(0);
    
                            MainActivity.getMainActivity().addScore(cardsMap[x][y].getNum());
                            merge = true;
                        }
    
                        break;
                    }
                }
            }
        }
    
        //只要有任意一行发生过卡片移动,则需要产生新的卡片
        if (merge) {
    
            addRandomNum();
            checkComplete();//判断当前游戏是否失败
        }
    }

    左移,针对面板中所有列,将每行的方块向左移动。在两种情况发生卡片合并:

    1 当前位置为空卡片,右侧为非空卡片,合并后当前位置卡片Num为右侧卡片,右侧卡片清零。

    2 当前位置为非空卡片,右侧卡片数值和它相等,合并后当前位置卡片数量翻倍,右侧卡片清零。

    从游戏角度来讲:1 对应卡片单纯的移动,2 对应两张相同卡片的合并。因此,只要发生卡片实质上的移动,就应该随机再生产一个卡片,调用addRandomNum()。

    2.4 游戏结束的判断

    每次发生卡片移动,都要检查游戏还能否继续,是否已经结束。函数checkComplete()完成游戏失败(感觉叫做checkFailure()更好)的检查:

    private void checkComplete(){
    
        boolean complete = true;
    
    ALL:
        for (int y = 0; y < Config.LINES; y++) {
            for (int x = 0; x < Config.LINES; x++) {
                //满足任意两个条件,游戏就可以继续:1 有空的格子,2 有可以合并的卡片
                if (cardsMap[x][y].getNum()==0||//1 有多余空间
                        (x>0&&cardsMap[x][y].equals(cardsMap[x-1][y]))||//2 和左面相等
                        (x<Config.LINES-1&&cardsMap[x][y].equals(cardsMap[x+1][y]))|//2 和右面相等
                        (y>0&&cardsMap[x][y].equals(cardsMap[x][y-1]))||//2 和上面相等
                        (y<Config.LINES-1&&cardsMap[x][y].equals(cardsMap[x][y+1]))) {//2 和下面相等
    
                    complete = false;
                    break ALL;
            }
        }
        if (complete) {
            new AlertDialog.Builder(getContext()).setTitle("你好").setMessage("游戏结束").setPositiveButton("重新开始", new DialogInterface.OnClickListener() {
    
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    startGame();
                }
            }).show();
        }
    
    }

    游戏可以继续的两个条件:有空的格子,或者还有能够合并的卡片。

  • 相关阅读:
    结队-贪吃蛇游戏-项目进度
    团队-象棋游戏-开发环境搭建过程
    团队-中国象棋游戏-设计文档
    结对-贪吃蛇游戏-开发环境搭建过程
    结对-结对编项目贪吃蛇-设计文档
    课后作业-阅读任务-阅读提问-1
    《20170911-构建之法:现代软件工程-阅读笔记》
    团队-中国象棋-成员简介及分工
    团队-团队编程项目中国象棋-需求分析
    结队-结队编程项目贪吃蛇--需求分析
  • 原文地址:https://www.cnblogs.com/ilfmonday/p/2048android.html
Copyright © 2020-2023  润新知