1. 程式人生 > >極客學院Android之2048遊戲開發全過程

極客學院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) {