极客学院Android之2048游戏开发全过程

2048完整开发

课1、游戏2048玩法介绍

同一条线上的相同数字折叠

课2、创建2048游戏项目

修改布局

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world" />

</LinearLayout>

准备MainActivity

package com.jikexueyuan.game2048;

import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }


    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

}

课3 设计2048游戏布局

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity" >

    <LinearLayout android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <TextView android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/score"/>
        <TextView android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/tvScore"/>
    </LinearLayout>

    <GridLayout 
        android:layout_width="fill_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:id="@+id/gameView"></GridLayout>

</LinearLayout>

课4 实现2048游戏主类GameView

新建一个GameView类继承GridView,目的为了把layout里的GridView替换掉而绑定该自定义的控件类

GameView.class

package com.jikexueyuan.game2048;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.GridLayout;

public class GameView extends GridLayout {
//我们为了让xml绑定该类,我们就把该类的全路径把原来的GridLayout替换掉
    /**
     * 这三个构造函数的创建是为了可以访问到layout里面的Gridview控件
     * @param context
     * @param attrs
     * @param defStyle
     */
    public GameView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        //自己手动添加的类的入口方法
        initGameView();
    }

    public GameView(Context context) {
        super(context);

        //自己手动添加的类的入口方法
        initGameView();
    }

    public GameView(Context context, AttributeSet attrs) {
        super(context, attrs);

        //自己手动添加的类的入口方法
        initGameView();
    }


    //自定义类的入口方法
    private void initGameView(){

    }
}

替换了GridView后的layout

<com.jikexueyuan.game2048.GameView 
    android:layout_width="fill_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    android:id="@+id/gameView"></com.jikexueyuan.game2048.GameView>

课5 游戏2048在Android平台的触控交互设计

在我们自定义的控件里加上侦听用户水平滑动垂直滑动控件空间里的操作代码

GameView

package com.jikexueyuan.game2048;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.GridLayout;

public class GameView extends GridLayout {
//我们为了让xml绑定该类,我们就把该类的全路径把原来的GridLayout替换掉
    /**
     * 这三个构造函数的创建是为了可以访问到layout里面的Gridview控件
     * @param context
     * @param attrs
     * @param defStyle
     */
    public GameView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        //自己手动添加的类的入口方法
        initGameView();
    }

    public GameView(Context context) {
        super(context);

        //自己手动添加的类的入口方法
        initGameView();
    }

    public GameView(Context context, AttributeSet attrs) {
        super(context, attrs);

        //自己手动添加的类的入口方法
        initGameView();
    }


    //自定义类的入口方法
    private void initGameView(){
        //如何创建手势操作呢?
        setOnTouchListener(new 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){//手势往左,-5表示范围
                            System.out.println("left操作");
                            swipeLeft();//侦听用户操作后执行该方法
                        }else if (offsetX>5){//往右
                            System.out.println("rigth操作");
                            swipeRight();//侦听用户操作后执行该方法
                        }
                    }else{//这个else判断用户的手势上下滑动不太偏离垂直线
                        if(offsetY<-5){//手势往左,-5表示范围
                            System.out.println("up操作");
                            swipeUp();//侦听用户操作后执行该方法
                        }else if (offsetY>5){//往右
                            System.out.println("down操作");
                            swipeDown();//侦听用户操作后执行该方法
                        }
                    }
                    break;
                }

                return true;
            }
        });
    }

    private void swipeLeft(){
        Toast.makeText(getContext(), "向左滑动了", 0).show();
    }
    private void swipeRight(){
        Toast.makeText(getContext(), "向右滑动了", 0).show();
    }
    private void swipeUp(){
        Toast.makeText(getContext(), "向上滑动了", 0).show();
    }
    private void swipeDown(){
        Toast.makeText(getContext(), "向下滑动了", 0).show();
    }
}


课6 实现2048游戏的卡片类

我们需要控制2048里面的卡片类,首先要创建出该卡片类

Card.class

package com.jikexueyuan.game2048;

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

public class Card extends FrameLayout {
    //首先该卡片类是一个FrameLayout控件类,即在该类里可以嵌入其他控件类例如文本控件什么的,所以当该类一实现了以后就会初始化文本控件,而通过构造函数里的初始化而同时初始化了文本控件的属性
    //构造函数:初始化
    public Card(Context context) {
        super(context);
        //初始化文本
        label = new TextView(getContext());
        //设置文本的大小
        label.setTextSize(32);

        //布局参数用来控制
        LayoutParams lp = new LayoutParams(-1,-1);//该类用来初始化layout控件textView里的高和宽属性
        addView(label, lp);//该方法是用来给label控件加上已经初始化了的高和宽属性
        setNum(0);
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
        //呈现
        label.setText(num+"");
    }

    private int num = 0;

    //需要呈现文字
    private TextView label;

    //判断两张卡片数字是否一样?
    public boolean equals(Card o) {
        return getNum()==o.getNum();
    }

}

课7 添加2048游戏卡片

往自定义布局GridView里添加卡片了

由于考虑到不同手机的宽高不一致,所以我们在布局里添加卡片的时候需要动态地去计算卡片的宽高根据具体的手机

布局

<application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.jikexueyuan.game2048.MainActivity"
            android:label="@string/app_name"
            android:screenOrientation="portrait" ><!-- 限制游戏发生横屏 -->

GameView.class

package com.jikexueyuan.game2048;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.GridLayout;
import android.widget.Toast;

public class GameView extends GridLayout {
//我们为了让xml绑定该类,我们就把该类的全路径把原来的GridLayout替换掉
    /**
     * 这三个构造函数的创建是为了可以访问到layout里面的Gridview控件
     * @param context
     * @param attrs
     * @param defStyle
     */
    public GameView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        //自己手动添加的类的入口方法
        initGameView();
    }

    public GameView(Context context) {
        super(context);

        //自己手动添加的类的入口方法
        initGameView();
    }

    public GameView(Context context, AttributeSet attrs) {
        super(context, attrs);

        //自己手动添加的类的入口方法
        initGameView();
    }


    //自定义类的入口方法
    private void initGameView(){
        //由于在添加完卡片之后发现在GameView里创建的16个卡片都排成一行了
        //因此在这里设置有多少列
        /**
         * ColumnCount is used only to generate default column/column indices 
         * when they are not specified by a component's layout parameters.
         */
        setColumnCount(4);

        //在给GameView的背景颜色加上颜色
        setBackgroundColor(0xffbbada0);

        //如何创建手势操作呢?
        setOnTouchListener(new 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){//手势往左,-5表示范围
                            System.out.println("left操作");
                            swipeLeft();//侦听用户操作后执行该方法
                        }else if (offsetX>5){//往右
                            System.out.println("rigth操作");
                            swipeRight();//侦听用户操作后执行该方法
                        }
                    }else{//这个else判断用户的手势上下滑动不太偏离垂直线
                        if(offsetY<-5){//手势往左,-5表示范围
                            System.out.println("up操作");
                            swipeUp();//侦听用户操作后执行该方法
                        }else if (offsetY>5){//往右
                            System.out.println("down操作");
                            swipeDown();//侦听用户操作后执行该方法
                        }
                    }
                    break;
                }

                return true;
            }
        });
    }

    //怎么动态地计算卡片的宽高呢?
    @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            //该方法就是宽高发生改变的时候我们可以得到当前的宽高是多少
            //该方法也是在游戏一被创建的时候就调用,也就是用来初始宽高的方法

            //获取手机较窄的长度,-10是用来间隔每个卡片的距离,用手机的宽除以4就是每个卡片的长度了
            int cardWidth = (Math.min(w, h)-10)/4;

            //在该方法初始化的时候新建16个卡片,以下是方法
            addCards(cardWidth,cardWidth);

        }

    private void addCards(int cardWidth, int cardHeight) {
        Card c;
        for (int y = 0; y < 4; y++) {
            for (int x = 0; x < 4; x++) {
                c = new Card(getContext());
                c.setNum(2);//给卡片设置初始化数字
                addView(c, cardWidth, cardHeight);

                //顺便把初始化时新建的卡片类存放到下面新建的二维数组里
                cardsMap[x][y] = c;
            }
        }
    }

    private void swipeLeft(){
        Toast.makeText(getContext(), "向左滑动了", 0).show();
    }
    private void swipeRight(){
        Toast.makeText(getContext(), "向右滑动了", 0).show();
    }
    private void swipeUp(){
        Toast.makeText(getContext(), "向上滑动了", 0).show();
    }
    private void swipeDown(){
        Toast.makeText(getContext(), "向下滑动了", 0).show();
    }

    //我们需要定义一个二维数组来记录GameView初始化时生成的16个卡片类
    private Card[][] cardsMap = new Card[4][4];
}

Card.class

package com.jikexueyuan.game2048;

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

public class Card extends FrameLayout {
    //首先该卡片类是一个FrameLayout控件类,即在该类里可以嵌入其他控件类例如文本控件什么的,所以当该类一实现了以后就会初始化文本控件,而通过构造函数里的初始化而同时初始化了文本控件的属性
    //构造函数:初始化
    public Card(Context context) {
        super(context);
        //初始化文本
        label = new TextView(getContext());
        //设置文本的大小
        label.setTextSize(32);

        //设置控件card的背景颜色
        label.setBackgroundColor(0x33ffffff);
        //把放在控件里的文本居中处理
        label.setGravity(Gravity.CENTER);

        //布局参数用来控制
        LayoutParams lp = new LayoutParams(-1,-1);//该类用来初始化layout控件textView里的高和宽属性
        //给每个textView的左和上设置margin,右和下就不需要了
        lp.setMargins(10, 10, 0, 0);
        addView(label, lp);//该方法是用来给label控件加上已经初始化了的高和宽属性
        setNum(0);
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
        //呈现
        label.setText(num+"");
    }

    private int num = 0;

    //需要呈现文字
    private TextView label;

    //判断两张卡片数字是否一样?
    public boolean equals(Card o) {
        return getNum()==o.getNum();
    }

}


课8 在2048游戏中添加随机数

1、要在Card的setNum方法里加上判断,用来判断导入的随机算不能大于0或小于0

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

    //由于我们需要在这个设置数字的方法导入随机数字,而为了排除出现0,我们需要在原方法里加上判断语句
    if(num<=0){
        label.setText("");
    }else{
        label.setText(num+"");
    }
//  label.setText(num+""); }

2、在GameView里加上设置随机数的方法

 void addRandomNum(){}

3、设置一个用来存放控制 Card[][] cardsMap 下标用的Point类的List集合

private List<Point> emptyPoints = new ArrayList<Point>();

4、通过这个集合在addRandomNum()方法里去控制每个card对象的文本属性

1、通过座标轴给Card[][] cardsMap 里的每个card对象绑定上了Point,换句话说就是用 Point来记录每个Card所在的座标轴,然后把Point存放在List集合里

//把这个point清空,每次调用添加随机数时就清空之前所控制的指针
emptyPoints.clear();

//对所有的位置进行遍历:即为每个卡片加上了可以控制的指针
for(int y = 0;y<4;y++){
    for (int x = 0; x < 4;x++) {
        if(cardsMap[x][y].getNum()<=0){
            emptyPoints.add(new Point(x,y));//给List存放控制卡片用的指针(通过座标轴来控制)
        }
    }
}

2、通过随机的控制而从存放了的Point的List集合里去获取Card的位置,并给这个card设置文本属性,并且只能存2或4,而且2的机率比4的大。

//一个for循环走完我们就从List里取出一个控制指针的point对象
Point p = emptyPoints.remove((int)(Math.random()*emptyPoints.size()));
//
cardsMap[p.x][p.y].setNum(Math.random()>0.1?2:4);//通过point对象来充当下标的角色来控制存放card的二维数组cardsMap,然后随机给定位到的card对象赋值

5、设置一个开始游戏的方法startGame():该方法会初始化16个card对象的状态

1、首先遍历了所有的card,给card的属性赋上0 

for (int y = 0; y < 4; y++) { 

    for (int x = 0; x < 4; x++) { 

        cardsMap[x][y].setNum(0);

    }
}

2、根据游戏规则去给这么多个card对象的其中两个加上随机数,也就是调用两次addRandomNum();方法

GameView.java

package com.jikexueyuan.game2048;

import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.graphics.Point;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.GridLayout;
import android.widget.Toast;

public class GameView extends GridLayout {
//我们为了让xml绑定该类,我们就把该类的全路径把原来的GridLayout替换掉
    /**
     * 这三个构造函数的创建是为了可以访问到layout里面的Gridview控件
     * @param context
     * @param attrs
     * @param defStyle
     */
    public GameView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        //自己手动添加的类的入口方法
        initGameView();
    }

    public GameView(Context context) {
        super(context);

        //自己手动添加的类的入口方法
        initGameView();
    }

    public GameView(Context context, AttributeSet attrs) {
        super(context, attrs);

        //自己手动添加的类的入口方法
        initGameView();
    }


    //自定义类的入口方法
    private void initGameView(){
        //由于在添加完卡片之后发现在GameView里创建的16个卡片都排成一行了
        //因此在这里设置有多少列
        /**
         * ColumnCount is used only to generate default column/column indices 
         * when they are not specified by a component's layout parameters.
         */
        setColumnCount(4);

        //在给GameView的背景颜色加上颜色
        setBackgroundColor(0xffbbada0);

        //如何创建手势操作呢?
        setOnTouchListener(new 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){//手势往左,-5表示范围
                            System.out.println("left操作");
                            swipeLeft();//侦听用户操作后执行该方法
                        }else if (offsetX>5){//往右
                            System.out.println("rigth操作");
                            swipeRight();//侦听用户操作后执行该方法
                        }
                    }else{//这个else判断用户的手势上下滑动不太偏离垂直线
                        if(offsetY<-5){//手势往左,-5表示范围
                            System.out.println("up操作");
                            swipeUp();//侦听用户操作后执行该方法
                        }else if (offsetY>5){//往右
                            System.out.println("down操作");
                            swipeDown();//侦听用户操作后执行该方法
                        }
                    }
                    break;
                }

                return true;
            }
        });
    }

    //怎么动态地计算卡片的宽高呢?
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //该方法就是宽高发生改变的时候我们可以得到当前的宽高是多少
        //该方法也是在游戏一被创建的时候就调用,也就是用来初始宽高的方法

        //获取手机较窄的长度,-10是用来间隔每个卡片的距离,用手机的宽除以4就是每个卡片的长度了
        int cardWidth = (Math.min(w, h)-10)/4;

        //在该方法初始化的时候新建16个卡片,以下是方法
        addCards(cardWidth,cardWidth);

         startGame();

    }


    private void addCards(int cardWidth, int cardHeight) {
        Card c;
        for (int y = 0; y < 4; y++) {
            for (int x = 0; x < 4; x++) {
                c = new Card(getContext());
                c.setNum(2);//给卡片设置初始化数字
                addView(c, cardWidth, cardHeight);

                //顺便把初始化时新建的卡片类存放到下面新建的二维数组里
                cardsMap[x][y] = c;
            }
        }
    }

    //现在可以开始使用这些随机数了
    private void startGame(){
        //清理阶段:初始化阶段
        for (int y = 0; y < 4; y++) {
            for (int x = 0; x < 4; x++) {
                cardsMap[x][y].setNum(0);

            }
        }
        //然后就是添加随机数阶段
        addRandomNum();
        addRandomNum();
    }

    //设置随机数的方法  
    private void addRandomNum(){
        //把这个point清空,每次调用添加随机数时就清空之前所控制的指针
        emptyPoints.clear();

        //对所有的位置进行遍历:即为每个卡片加上了可以控制的指针
        for(int y = 0;y<4;y++){
            for (int x = 0; x < 4;x++) {
                if(cardsMap[x][y].getNum()<=0){
                    emptyPoints.add(new Point(x,y));//给List存放控制卡片用的指针(通过座标轴来控制)
                }
            }
        }
        //一个for循环走完我们就从List里取出一个控制指针的point对象
        Point p = emptyPoints.remove((int)(Math.random()*emptyPoints.size()));
        //
        cardsMap[p.x][p.y].setNum(Math.random()>0.1?2:4);//通过point对象来充当下标的角色来控制存放card的二维数组cardsMap,然后随机给定位到的card对象赋值
    }

    private void swipeLeft(){
        Toast.makeText(getContext(), "向左滑动了", 0).show();
    }
    private void swipeRight(){
        Toast.makeText(getContext(), "向右滑动了", 0).show();
    }
    private void swipeUp(){
        Toast.makeText(getContext(), "向上滑动了", 0).show();
    }
    private void swipeDown(){
        Toast.makeText(getContext(), "向下滑动了", 0).show();
    }

    //我们需要定义一个二维数组来记录GameView初始化时生成的16个卡片类
    private Card[][] cardsMap = new Card[4][4];
    //我们添加一个list来存放Point来控制随机数方法里的随机数
    //注意这里有一个Point的类不熟悉
    private List<Point> emptyPoints = new ArrayList<Point>();
}

Card.java

package com.jikexueyuan.game2048;

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

public class Card extends FrameLayout {
    //首先该卡片类是一个FrameLayout控件类,即在该类里可以嵌入其他控件类例如文本控件什么的,所以当该类一实现了以后就会初始化文本控件,而通过构造函数里的初始化而同时初始化了文本控件的属性
    //构造函数:初始化
    public Card(Context context) {
        super(context);
        //初始化文本
        label = new TextView(getContext());
        //设置文本的大小
        label.setTextSize(32);

        //设置控件card的背景颜色
        label.setBackgroundColor(0x33ffffff);
        //把放在控件里的文本居中处理
        label.setGravity(Gravity.CENTER);

        //布局参数用来控制
        LayoutParams lp = new LayoutParams(-1,-1);//该类用来初始化layout控件textView里的高和宽属性
        //给每个textView的左和上设置margin,右和下就不需要了
        lp.setMargins(10, 10, 0, 0);
        addView(label, lp);//该方法是用来给label控件加上已经初始化了的高和宽属性
        setNum(0);
    }

    public int getNum() {
        return num;
    }

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

        //由于我们需要在这个设置数字的方法导入随机数字,而为了排除出现0,我们需要在原方法里加上判断语句
        if(num<=0){
            label.setText("");
        }else{
            label.setText(num+"");
        }
//      label.setText(num+"");
    }

    private int num = 0;

    //需要呈现文字
    private TextView label;

    //判断两张卡片数字是否一样?
    public boolean equals(Card o) {
        return getNum()==o.getNum();
    }

}


课9 实现2048游戏逻辑

private void swipeLeft(){
    //Toast.makeText(getContext(), "向左滑动了", 0).show();
    //以下两行for循环实现了一行一行的遍历,在向左滑动的时候
    for (int y = 0; y < 4; y++) {
        for (int x = 0; x < 4; x++) {

            for (int x1 = x+1; x1 < 4; x1++) {
                //这是在水平上固定了一个格子之后再继续在水平上遍历别的格子,且当格子有数的时候进行以下的操作
                if (cardsMap[x1][y].getNum()>0) {
                    //现在判断被固定的格子有没有数字,如果没有数字就进行以下的操作
                    if (cardsMap[x][y].getNum()<=0) {
                        //把与被固定的格子的同一水平上的格子的数字赋给被固定的格子
                        cardsMap[x][y].setNum(cardsMap[x1][y].getNum());
                        //把值赋给被固定的格子后自己归零
                        cardsMap[x1][y].setNum(0);
                        //第二层循环,即同一层的不同列退一格继续循环,这样做的原因是为了继续固定这个格子而去检查与它同一水平上的别的格子的内容,因为其他格子是什么个情况还需要继续在第二层进行判断
                        x--;
                        //这个break为了在操作完这固定格子遍历的过程操作完后跳出遍历,因为只要有操作这个条件,就没有继续遍历下去的需要了
                        break;

                    }else if (cardsMap[x][y].equals(cardsMap[x1][y])) {//这层判断是判断相邻两个数相同的情况
                        cardsMap[x][y].setNum(cardsMap[x][y].getNum()*2);
                        cardsMap[x1][y].setNum(0);
                        break;
                    }
                }
            }
        }
    }

}
private void swipeRight(){
    //Toast.makeText(getContext(), "向右滑动了", 0).show();
    for (int y = 0; y < 4; y++) {
        for (int x = 4-1; x >=0; x--) {

            for (int x1 = x-1; x1 >=0; x1--) {
                if (cardsMap[x1][y].getNum()>0) {

                    if (cardsMap[x][y].getNum()<=0) {
                        cardsMap[x][y].setNum(cardsMap[x1][y].getNum());
                        cardsMap[x1][y].setNum(0);
                        x++;
                        break;
                    }else if (cardsMap[x][y].equals(cardsMap[x1][y])) {
                        cardsMap[x][y].setNum(cardsMap[x][y].getNum()*2);
                        cardsMap[x1][y].setNum(0);
                        break;
                    }
                }
            }
        }
    }
}
private void swipeUp(){
    //Toast.makeText(getContext(), "向上滑动了", 0).show();
    for (int x = 0; x < 4; x++) {
        for (int y = 0; y < 4; y++) {

            for (int y1 = y+1; y1 < 4; y1++) {
                if (cardsMap[x][y1].getNum()>0) {

                    if (cardsMap[x][y].getNum()<=0) {
                        cardsMap[x][y].setNum(cardsMap[x][y1].getNum());
                        cardsMap[x][y1].setNum(0);
                        y--;
                        break;
                    }else if (cardsMap[x][y].equals(cardsMap[x][y1])) {
                        cardsMap[x][y].setNum(cardsMap[x][y].getNum()*2);
                        cardsMap[x][y1].setNum(0);
                        break;
                    }
                }
            }
        }
    }
}
private void swipeDown(){
    Toast.makeText(getContext(), "向下滑动了", 0).show();
    for (int x = 0; x < 4; x++) {
        for (int y = 4-1; y >=0; y--) {

            for (int y1 = y-1; y1 >=0; y1--) {
                if (cardsMap[x][y1].getNum()>0) {

                    if (cardsMap[x][y].getNum()<=0) {
                        cardsMap[x][y].setNum(cardsMap[x][y1].getNum());
                        cardsMap[x][y1].setNum(0);

                        y++;
                        break;
                    }else if (cardsMap[x][y].equals(cardsMap[x][y1])) {
                        cardsMap[x][y].setNum(cardsMap[x][y].getNum()*2);
                        cardsMap[x][y1].setNum(0);
                        break;
                    }

                }
            }
        }
    }
}



课10 游戏2048计分

1、在MainActivity中的Score(TextView控件)进行分数显示,并加上方法去控制分数

package com.jikexueyuan.game2048;

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

public class MainActivity extends Activity {
    private TextView tvScore;
    private int score = 0;

    //提供一个单例设计模式给别的类去调用该类中的处理分数的方法
    private static MainActivity mainActivity = null;
    public MainActivity(){
        mainActivity = this;
    }
    public static MainActivity getMainActivity(){
        return mainActivity;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tvScore = (TextView) findViewById(R.id.tvScore);
    }

    //分数清零
    public void clearScore(){
        score = 0;
        showScore();
    }

    //在控件上显示分数
    public void showScore(){
        tvScore.setText(score+"");
    }

    //使用方法添加分数,并显示出来
    public void addScore(int s){
        score+=s;
        showScore();
    }
}

2、在GameView里通过MainActivity里的单例思想获得对象去操作MainActivity里的操作分数的方法,并在滑动手势里进行识别加分,且继续增加随机数

private void swipeLeft(){
        boolean merge = false;//控制每滑动一次画面就加一个随机数到画面,也就是在下面所有for循环之后
//      Toast.makeText(getContext(), "向左滑动了", 0).show();
        //以下两行for循环实现了一行一行的遍历,在向左滑动的时候
        for (int y = 0; y < 4; y++) {
            for (int x = 0; x < 4; x++) {

                for (int x1 = x+1; x1 < 4; x1++) {
                    //这是在水平上固定了一个格子之后再继续在水平上遍历别的格子,且当格子有数的时候进行以下的操作
                    if (cardsMap[x1][y].getNum()>0) {
                        //现在判断被固定的格子有没有数字,如果没有数字就进行以下的操作
                        if (cardsMap[x][y].getNum()<=0) {
                            //把与被固定的格子的同一水平上的格子的数字赋给被固定的格子
                            cardsMap[x][y].setNum(cardsMap[x1][y].getNum());
                            //把值赋给被固定的格子后自己归零
                            cardsMap[x1][y].setNum(0);
                            //第二层循环,即同一层的不同列退一格继续循环,这样做的原因是为了继续固定这个格子而去检查与它同一水平上的别的格子的内容,因为其他格子是什么个情况还需要继续在第二层进行判断
                            x--;
                            //只要有移动就要加随机数
                            merge = true;

                        }else if (cardsMap[x][y].equals(cardsMap[x1][y])) {//这层判断是判断相邻两个数相同的情况
                            cardsMap[x][y].setNum(cardsMap[x][y].getNum()*2);
                            cardsMap[x1][y].setNum(0);
                            //在这要给MainActivity中的score加上分数
                            //而这里MainActivity设计成了单例设计模式,所以要使用get方法获得对象
                            MainActivity.getMainActivity().addScore(cardsMap[x][y].getNum());
                            //只要有移动就要加随机数
                            merge = true;
                        }
                        //这个break为了在操作完这固定格子遍历的过程操作完后跳出遍历,因为只要有操作这个条件,就没有继续遍历下去的需要了
                        break;
                    }
                }
            }
        }
        if (merge) {
            addRandomNum();
        }
    }



课11 检查2048游戏结束

两个条件来判断结束

1、所有的格子都有数字 

2、所有相邻的格子都没有相同的数字

/*
 * 
 * 方法放在滑动手势里,每次滑动是用来判断每个格子的上下左右没有相同的数字,和格子是否为空,以此弹出对话框来提示用户并调用重新开始方法
 */
private void checkComplete(){

    boolean complete = true;

    ALL:
        for (int y = 0; y < 4; y++) {
            for (int x = 0; x < 4; x++) {//遍历格子
                if (cardsMap[x][y].getNum()==0||
                        (x>0&&cardsMap[x][y].equals(cardsMap[x-1][y]))||
                        (x<4-1&&cardsMap[x][y].equals(cardsMap[x+1][y]))||
                        (y>0&&cardsMap[x][y].equals(cardsMap[x][y-1]))||
                        (y<4-1&&cardsMap[x][y].equals(cardsMap[x][y+1]))) {

                    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();
    }



工程代码:

布局

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity" >
    <LinearLayout android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <TextView android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/score"/>
        <TextView android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/tvScore"/>
    </LinearLayout>

    <com.jikexueyuan.game2048.GameView 
        android:layout_width="fill_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:id="@+id/gameView"></com.jikexueyuan.game2048.GameView>

</LinearLayout>

Card.java

package com.jikexueyuan.game2048;

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

public class Card extends FrameLayout {
    //首先该卡片类是一个FrameLayout控件类,即在该类里可以嵌入其他控件类例如文本控件什么的,所以当该类一实现了以后就会初始化文本控件,而通过构造函数里的初始化而同时初始化了文本控件的属性
    //构造函数:初始化
    public Card(Context context) {
        super(context);
        //初始化文本
        label = new TextView(getContext());
        //设置文本的大小
        label.setTextSize(32);

        //设置控件card的背景颜色
        label.setBackgroundColor(0x33ffffff);
        //把放在控件里的文本居中处理
        label.setGravity(Gravity.CENTER);

        //布局参数用来控制
        LayoutParams lp = new LayoutParams(-1,-1);//该类用来初始化layout控件textView里的高和宽属性
        //给每个textView的左和上设置margin,右和下就不需要了
        lp.setMargins(10, 10, 0, 0);
        addView(label, lp);//该方法是用来给label控件加上已经初始化了的高和宽属性
        setNum(0);
    }

    public int getNum() {
        return num;
    }

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

        //由于我们需要在这个设置数字的方法导入随机数字,而为了排除出现0,我们需要在原方法里加上判断语句
        if(num<=0){
            label.setText("");
        }else{
            label.setText(num+"");
        }
//      label.setText(num+"");
    }

    private int num = 0;

    //需要呈现文字
    private TextView label;

    //判断两张卡片数字是否一样?
    public boolean equals(Card o) {
        return getNum()==o.getNum();
    }

}

GameView.java

package com.jikexueyuan.game2048;

import java.util.ArrayList;
import java.util.List;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Point;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.GridLayout;

public class GameView extends GridLayout {
//我们为了让xml绑定该类,我们就把该类的全路径把原来的GridLayout替换掉
    /**
     * 这三个构造函数的创建是为了可以访问到layout里面的Gridview控件
     * @param context
     * @param attrs
     * @param defStyle
     */
    public GameView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        //自己手动添加的类的入口方法
        initGameView();
    }

    public GameView(Context context) {
        super(context);

        //自己手动添加的类的入口方法
        initGameView();
    }

    public GameView(Context context, AttributeSet attrs) {
        super(context, attrs);

        //自己手动添加的类的入口方法
        initGameView();
    }


    //自定义类的入口方法
    private void initGameView(){
        //由于在添加完卡片之后发现在GameView里创建的16个卡片都排成一行了
        //因此在这里设置有多少列
        /**
         * ColumnCount is used only to generate default column/column indices 
         * when they are not specified by a component's layout parameters.
         */
        setColumnCount(4);

        //在给GameView的背景颜色加上颜色
        setBackgroundColor(0xffbbada0);

        //如何创建手势操作呢?
        setOnTouchListener(new 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){//手势往左,-5表示范围
                            System.out.println("left操作");
                            swipeLeft();//侦听用户操作后执行该方法
                        }else if (offsetX>5){//往右
                            System.out.println("rigth操作");
                            swipeRight();//侦听用户操作后执行该方法
                        }
                    }else{//这个else判断用户的手势上下滑动不太偏离垂直线
                        if(offsetY<-5){//手势往左,-5表示范围
                            System.out.println("up操作");
                            swipeUp();//侦听用户操作后执行该方法
                        }else if (offsetY>5){//往右
                            System.out.println("down操作");
                            swipeDown();//侦听用户操作后执行该方法
                        }
                    }
                    break;
                }

                return true;
            }
        });
    }

    //怎么动态地计算卡片的宽高呢?
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //该方法就是宽高发生改变的时候我们可以得到当前的宽高是多少
        //该方法也是在游戏一被创建的时候就调用,也就是用来初始宽高的方法

        //获取手机较窄的长度,-10是用来间隔每个卡片的距离,用手机的宽除以4就是每个卡片的长度了
        int cardWidth = (Math.min(w, h)-10)/4;

        //在该方法初始化的时候新建16个卡片,以下是方法
        addCards(cardWidth,cardWidth);

         startGame();

    }


    private void addCards(int cardWidth, int cardHeight) {
        Card c;
        for (int y = 0; y < 4; y++) {
            for (int x = 0; x < 4; x++) {
                c = new Card(getContext());
                c.setNum(2);//给卡片设置初始化数字
                addView(c, cardWidth, cardHeight);

                //顺便把初始化时新建的卡片类存放到下面新建的二维数组里
                cardsMap[x][y] = c;
            }
        }
    }

    //现在可以开始使用这些随机数了
    private void startGame(){
        //重新开始的时候分数清零
        MainActivity aty = MainActivity.getMainActivity();
        aty.clearScore();
        //清理阶段:初始化阶段
        for (int y = 0; y < 4; y++) {
            for (int x = 0; x < 4; x++) {
                cardsMap[x][y].setNum(0);

            }
        }
        //然后就是添加随机数阶段
        addRandomNum();
        addRandomNum();
//      addRandomNum();
//      addRandomNum();
//      addRandomNum();
//      addRandomNum();
//      addRandomNum();
//      addRandomNum();
    }

    //设置随机数的方法  
    private void addRandomNum(){
        //把这个point清空,每次调用添加随机数时就清空之前所控制的指针
        emptyPoints.clear();

        //对所有的位置进行遍历:即为每个卡片加上了可以控制的指针
        for(int y = 0;y<4;y++){
            for (int x = 0; x < 4;x++) {
                if(cardsMap[x][y].getNum()<=0){
                    emptyPoints.add(new Point(x,y));//给List存放控制卡片用的指针(通过座标轴来控制)
                }
            }
        }
        //一个for循环走完我们就从List里取出一个控制指针的point对象
        Point p = emptyPoints.remove((int)(Math.random()*emptyPoints.size()));
        //
        cardsMap[p.x][p.y].setNum(Math.random()>0.1?2:4);//通过point对象来充当下标的角色来控制存放card的二维数组cardsMap,然后随机给定位到的card对象赋值
    }

    private void swipeLeft(){
        boolean merge = false;//控制每滑动一次画面就加一个随机数到画面,也就是在下面所有for循环之后
//      Toast.makeText(getContext(), "向左滑动了", 0).show();
        //以下两行for循环实现了一行一行的遍历,在向左滑动的时候
        for (int y = 0; y < 4; y++) {
            for (int x = 0; x < 4; x++) {

                for (int x1 = x+1; x1 < 4; x1++) {
                    //这是在水平上固定了一个格子之后再继续在水平上遍历别的格子,且当格子有数的时候进行以下的操作
                    if (cardsMap[x1][y].getNum()>0) {
                        //现在判断被固定的格子有没有数字,如果没有数字就进行以下的操作
                        if (cardsMap[x][y].getNum()<=0) {
                            //把与被固定的格子的同一水平上的格子的数字赋给被固定的格子
                            cardsMap[x][y].setNum(cardsMap[x1][y].getNum());
                            //把值赋给被固定的格子后自己归零
                            cardsMap[x1][y].setNum(0);
                            //第二层循环,即同一层的不同列退一格继续循环,这样做的原因是为了继续固定这个格子而去检查与它同一水平上的别的格子的内容,因为其他格子是什么个情况还需要继续在第二层进行判断
                            x--;
                            //只要有移动就要加随机数
                            merge = true;

                        }else if (cardsMap[x][y].equals(cardsMap[x1][y])) {//这层判断是判断相邻两个数相同的情况
                            cardsMap[x][y].setNum(cardsMap[x][y].getNum()*2);
                            cardsMap[x1][y].setNum(0);
                            //在这要给MainActivity中的score加上分数
                            //而这里MainActivity设计成了单例设计模式,所以要使用get方法获得对象
                            MainActivity.getMainActivity().addScore(cardsMap[x][y].getNum());
                            //只要有移动就要加随机数
                            merge = true;
                        }
                        //这个break为了在操作完这固定格子遍历的过程操作完后跳出遍历,因为只要有操作这个条件,就没有继续遍历下去的需要了
                        break;
                    }
                }
            }
        }
        if (merge) {
            addRandomNum();
            checkComplete();
        }
    }
    private void swipeRight(){
        boolean merge = false;//控制每滑动一次画面就加一个随机数到画面,也就是在下面所有for循环之后
//      Toast.makeText(getContext(), "向右滑动了", 0).show();
        for (int y = 0; y < 4; y++) {
            for (int x = 4-1; x >=0; x--) {

                for (int x1 = x-1; x1 >=0; x1--) {
                    if (cardsMap[x1][y].getNum()>0) {

                        if (cardsMap[x][y].getNum()<=0) {
                            cardsMap[x][y].setNum(cardsMap[x1][y].getNum());
                            cardsMap[x1][y].setNum(0);
                            x++;
                            //只要有移动就要加随机数
                            merge = true;
                        }else if (cardsMap[x][y].equals(cardsMap[x1][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();
        }
    }
    private void swipeUp(){
        boolean merge = false;//控制每滑动一次画面就加一个随机数到画面,也就是在下面所有for循环之后
//      Toast.makeText(getContext(), "向上滑动了", 0).show();
        for (int x = 0; x < 4; x++) {
            for (int y = 0; y < 4; y++) {

                for (int y1 = y+1; y1 < 4; y1++) {
                    if (cardsMap[x][y1].getNum()>0) {

                        if (cardsMap[x][y].getNum()<=0) {
                            cardsMap[x][y].setNum(cardsMap[x][y1].getNum());
                            cardsMap[x][y1].setNum(0);
                            y--;
                            //只要有移动就要加随机数
                            merge = true;
                        }else if (cardsMap[x][y].equals(cardsMap[x][y1])) {
                            cardsMap[x][y].setNum(cardsMap[x][y].getNum()*2);
                            cardsMap[x][y1].setNum(0);
                            MainActivity.getMainActivity().addScore(cardsMap[x][y].getNum());
                            //只要有移动就要加随机数
                            merge = true;
                        }
                        break;
                    }
                }
            }
        }
        if (merge) {
            addRandomNum();
            checkComplete();
        }
    }
    private void swipeDown(){
        boolean merge = false;//控制每滑动一次画面就加一个随机数到画面,也就是在下面所有for循环之后
//      Toast.makeText(getContext(), "向下滑动了", 0).show();
        for (int x = 0; x < 4; x++) {
            for (int y = 4-1; y >=0; y--) {

                for (int y1 = y-1; y1 >=0; y1--) {
                    if (cardsMap[x][y1].getNum()>0) {

                        if (cardsMap[x][y].getNum()<=0) {
                            cardsMap[x][y].setNum(cardsMap[x][y1].getNum());
                            cardsMap[x][y1].setNum(0);

                            y++;
                            //只要有移动就要加随机数
                            merge = true;
                        }else if (cardsMap[x][y].equals(cardsMap[x][y1])) {
                            cardsMap[x][y].setNum(cardsMap[x][y].getNum()*2);
                            cardsMap[x][y1].setNum(0);
                            MainActivity.getMainActivity().addScore(cardsMap[x][y].getNum());
                            //只要有移动就要加随机数
                            merge = true;
                        }
                        break;

                    }
                }
            }
        }
        if (merge) {
            addRandomNum();
            checkComplete();
        }
    }

    /*
     * 
     * 方法放在滑动手势里,每次滑动是用来判断每个格子的上下左右没有相同的数字,和格子是否为空,以此弹出对话框来提示用户并调用重新开始方法
     */
    private void checkComplete(){

        boolean complete = true;

        ALL:
            for (int y = 0; y < 4; y++) {
                for (int x = 0; x < 4; x++) {//遍历格子
                    if (cardsMap[x][y].getNum()==0||
                            (x>0&&cardsMap[x][y].equals(cardsMap[x-1][y]))||
                            (x<4-1&&cardsMap[x][y].equals(cardsMap[x+1][y]))||
                            (y>0&&cardsMap[x][y].equals(cardsMap[x][y-1]))||
                            (y<4-1&&cardsMap[x][y].equals(cardsMap[x][y+1]))) {

                        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();
        }

    }

    //我们需要定义一个二维数组来记录GameView初始化时生成的16个卡片类
    private Card[][] cardsMap = new Card[4][4];
    //我们添加一个list来存放Point来控制随机数方法里的随机数
    //注意这里有一个Point的类不熟悉
    private List<Point> emptyPoints = new ArrayList<Point>();
}

MainActivity.class

package com.jikexueyuan.game2048;

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

public class MainActivity extends Activity {
    private TextView tvScore;
    private int score = 0;

    //提供一个单例设计模式给别的类去调用该类中的处理分数的方法
    private static MainActivity mainActivity = null;
    public MainActivity(){
        mainActivity = this;
    }
    public static MainActivity getMainActivity(){
        return mainActivity;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tvScore = (TextView) findViewById(R.id.tvScore);
    }

    //分数清零
    public void clearScore(){
        score = 0;
        showScore();
    }

    //在控件上显示分数
    public void showScore(){
        tvScore.setText(score+"");
    }

    //使用方法添加分数,并显示出来
    public void addScore(int s){
        score+=s;
        showScore();
    }
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.jikexueyuan.game2048"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="14"
        android:targetSdkVersion="17" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.jikexueyuan.game2048.MainActivity"
            android:label="@string/app_name"
            android:screenOrientation="portrait" ><!-- 限制游戏发生横屏 -->

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
资料下载
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章