Android 帶你玩轉實現游戲2048 其實2048只是個普通的控件

jopen 8年前發布 | 14K 次閱讀 Android開發 移動開發

轉載請標明出處:http://blog.csdn.net/lmj623565791/article/details/40020137,本文出自:【張鴻洋的博客】

1、概述

博主本想踏入游戲開放行業,無奈水太深,不會游泳;于是乎,只能繼續開發應用,但是原生Android也能開發游戲么,2048、像素鳥、別踩什么來著;今天給大家帶來一篇2048的開發篇,別怕不分上下文,或者1、2、3、4,一篇包你能玩happy~雖然我從來沒有玩到過2048!!!其實大家也可以當作自定義控件來看~~~

特別說明一下,游戲2048里面的方塊各種顏色來源于:http://download.csdn.net/detail/qq1121674367/7155467,這個2048的代碼中,其他代碼,太多,未參考;特此感謝分享;大家也可以下載下,對比學習下;

接下來貼個我們項目的效果圖:

ok 看完效果圖,我就準備帶領大家征服這款游戲了~~~

2、實現分析

貼一張靜態圖,開始對我們游戲的設計:

可以看到,游戲其實就是一個容器,里面很多個方塊,觸摸容器,里面的方塊的形態會發生變化。那么:

1、容器我們準備自定義ViewGroup ,叫做Game2048Layout ; 里面的塊塊自定義View ,叫做Game2048Item

接下來從簡單的開始:

2、Game2048Item

Game2048Item是個View,并且需要哪些屬性呢?

首先得有個number,顯示數字嘛,然后繪制的時候根據number繪制背景色;還需要呢?嗯,需要正方形邊長,再考慮下,這個邊長應該Item自己控制么?顯然不是的,Game2048Layout 是個n*n的面板,這個n是不確定的,所以Item的邊長肯定是Game2048Layout 計算好傳入的。這樣必須的屬性就這兩個。

3、Game2048Layout

 Game2048Layout是個容器,我們觀察下,里面View是個 n*n的排列,我們準備讓其繼承RelativeLayout ; 這樣可以通過設置Item的RIGHT_OF之類的屬性進行定位;

我們在onMeasure里面得到Layout的寬和高,然后根據n*n,生成一定數目的Item,為其設置寬和高,放置到Layout中,這樣整個游戲的布局就做好了;繪制的細節上:Item間有橫向與縱向的間距,所以需要設置這個值,叫做mMargin。然后Item的邊長 =  ( Layout邊長 - (n-1)*mMagin ) / n ; 

剩下的就是onTouchEvent里面去判斷用戶手勢了,然后就行各種邏輯操作了~

3、代碼之旅

首先來看看我們的Game2048Item

1、Game2048Item

package com.zhy.game2048.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

/**
 * 2048的每個Item
 * 
 * @author zhy
 * 
 */
public class Game2048Item extends View
{
    /**
     * 該View上的數字
     */
    private int mNumber;
    private String mNumberVal;
    private Paint mPaint;
    /**
     * 繪制文字的區域
     */
    private Rect mBound;

    public Game2048Item(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);
        mPaint = new Paint();

    }

    public Game2048Item(Context context)
    {
        this(context, null);
    }

    public Game2048Item(Context context, AttributeSet attrs)
    {
        this(context, attrs, 0);
    }

    public void setNumber(int number)
    {
        mNumber = number;
        mNumberVal = mNumber + "";
        mPaint.setTextSize(30.0f);
        mBound = new Rect();
        mPaint.getTextBounds(mNumberVal, 0, mNumberVal.length(), mBound);
        invalidate();
    }



    public int getNumber()
    {
        return mNumber;
    }

    @Override
    protected void onDraw(Canvas canvas)
    {

        super.onDraw(canvas);
        String mBgColor = "";
        switch (mNumber)
        {
        case 0:
            mBgColor = "#CCC0B3";
            break;
        case 2:
            mBgColor = "#EEE4DA";
            break;
        case 4:
            mBgColor = "#EDE0C8";
            break;
        case 8:
            mBgColor = "#F2B179";// #F2B179
            break;
        case 16:
            mBgColor = "#F49563";
            break;
        case 32:
            mBgColor = "#F5794D";
            break;
        case 64:
            mBgColor = "#F55D37";
            break;
        case 128:
            mBgColor = "#EEE863";
            break;
        case 256:
            mBgColor = "#EDB04D";
            break;
        case 512:
            mBgColor = "#ECB04D";
            break;
        case 1024:
            mBgColor = "#EB9437";
            break;
        case 2048:
            mBgColor = "#EA7821";
            break;
        default:
            mBgColor = "#EA7821";
            break;
        }

        mPaint.setColor(Color.parseColor(mBgColor));
        mPaint.setStyle(Style.FILL);
        canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);

        if (mNumber != 0)
            drawText(canvas);

    }

    /**
     * 繪制文字
     * 
     * @param canvas
     */
    private void drawText(Canvas canvas)
    {

        mPaint.setColor(Color.BLACK);
        float x = (getWidth() - mBound.width()) / 2;
        float y = getHeight() / 2 + mBound.height() / 2;
        canvas.drawText(mNumberVal, x, y, mPaint);
    }

}

很簡單,基本就一個onDraw通過number來繪制背景和數字;number通過調用setNumer進行設置;它的寬和高都是固定值,所以我們并不需要自己進行測量~~

2、Game2048Layout

1、成員變量

這就是我們最主要的一個類了,首先我們看看這個類的成員變量,先看看各個成員變量的作用:

/**
     * 設置Item的數量n*n;默認為4
     */
    private int mColumn = 4;
    /**
     * 存放所有的Item
     */
    private Game2048Item[] mGame2048Items;

    /**
     * Item橫向與縱向的邊距
     */
    private int mMargin = 10;
    /**
     * 面板的padding
     */
    private int mPadding;
    /**
     * 檢測用戶滑動的手勢
     */
    private GestureDetector mGestureDetector;

    // 用于確認是否需要生成一個新的值
    private boolean isMergeHappen = true;
    private boolean isMoveHappen = true;

    /**
     * 記錄分數
     */
    private int mScore;

主要的成員變量就這些,直接看注釋也比較容易理解~~

了解了成員變量,接下來我們需要在構造方法里面得到一些值和初始化一些變量

2、構造方法

public Game2048Layout(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);

        mMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                mMargin, getResources().getDisplayMetrics());
        // 設置Layout的內邊距,四邊一致,設置為四內邊距中的最小值
        mPadding = min(getPaddingLeft(), getPaddingTop(), getPaddingRight(),
                getPaddingBottom());

        mGestureDetector = new GestureDetector(context , new MyGestureDetector());

    }

我們在構造方法里面得到Item間的邊距(margin)和我們容器的內邊距(padding,),這個值應該四邊一致,于是我們取四邊的最小值;這兩個屬性可以抽取為自定義的屬性;然后初始化了我們的mGestureDetector

有了margin和padding,我們就可以計算我們item的邊長了。這個計算過程肯定在onMeasure里面,因為我們需要在onMeasure獲取容器的寬和高

 

3、onMeasure

private boolean once;

    /**
     * 測量Layout的寬和高,以及設置Item的寬和高,這里忽略wrap_content 以寬、高之中的最小值繪制正方形
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 獲得正方形的邊長
        int length = Math.min(getMeasuredHeight(), getMeasuredWidth());
        // 獲得Item的寬度
        int childWidth = (length - mPadding * 2 - mMargin * (mColumn - 1))
                / mColumn;

        if (!once)
        {
            if (mGame2048Items == null)
            {
                mGame2048Items = new Game2048Item[mColumn * mColumn];
            }
            // 放置Item
            for (int i = 0; i < mGame2048Items.length; i++)
            {

                Game2048Item item = new Game2048Item(getContext());

                mGame2048Items[i] = item;
                item.setId(i + 1);
                RelativeLayout.LayoutParams lp = new LayoutParams(childWidth,
                        childWidth);
                // 設置橫向邊距,不是最后一列
                if ((i + 1) % mColumn != 0)
                {
                    lp.rightMargin = mMargin;
                }
                // 如果不是第一列
                if (i % mColumn != 0)
                {
                    lp.addRule(RelativeLayout.RIGHT_OF,//
                            mGame2048Items[i - 1].getId());
                }
                // 如果不是第一行,//設置縱向邊距,非最后一行
                if ((i + 1) > mColumn)
                {
                    lp.topMargin = mMargin;
                    lp.addRule(RelativeLayout.BELOW,//
                            mGame2048Items[i - mColumn].getId());
                }
                addView(item, lp);
            }
            generateNum();
        }
        once = true;

        setMeasuredDimension(length, length);
    }

首先設置容器的邊長為寬高中的最小值;然后(length - mPadding * 2 - mMargin * (mColumn - 1)) / mColumn ; 去計算Item的邊長;

拿到以后,根據我們的mColumn初始化我們的Item數組,然后遍歷生成Item,設置Item的LayoutParams以及Rule(RIGHT_OF , BELOW),最后添加到我們的容器中;

最后我們通過setMeasuredDimension(length, length);改變我們布局占據的空間;

到此,我們整個面板繪制完成了;

接下來,就是根據用戶的手勢,去進行游戲邏輯操作了,手勢那么肯定是onTouchEvent了:

4、onTouchEvent

@Override
    public boolean onTouchEvent(MotionEvent event)
    {
        mGestureDetector.onTouchEvent(event);
        return true;
    }

我們把觸摸事件交給了mGestureDetector,我們去看看我們的mGestureDetector,在構造方法中有這么一句:

mGestureDetector = new GestureDetector(context , new MyGestureDetector());

so,我們需要去看看MyGestureDetector:

class MyGestureDetector extends GestureDetector.SimpleOnGestureListener
    {

        final int FLING_MIN_DISTANCE = 50;

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
                float velocityY)
        {
            float x = e2.getX() - e1.getX();
            float y = e2.getY() - e1.getY();

            if (x > FLING_MIN_DISTANCE
                    && Math.abs(velocityX) > Math.abs(velocityY))
            {
                action(ACTION.RIGHT);

            } else if (x < -FLING_MIN_DISTANCE
                    && Math.abs(velocityX) > Math.abs(velocityY))
            {
                action(ACTION.LEFT);

            } else if (y > FLING_MIN_DISTANCE
                    && Math.abs(velocityX) < Math.abs(velocityY))
            {
                action(ACTION.DOWM);

            } else if (y < -FLING_MIN_DISTANCE
                    && Math.abs(velocityX) < Math.abs(velocityY))
            {
                action(ACTION.UP);
            }
            return true;

        }

    }

很簡單,就是判讀用戶上、下、左、右滑動;然后去調用action(ACTION)方法;ACTION是個枚舉:

/**
     * 運動方向的枚舉
     * 
     * @author zhy
     * 
     */
    private enum ACTION
    {
        LEFT, RIGHT, UP, DOWM
    }

這么看,核心代碼都在action方法里面了:

5、根據用戶手勢重繪Item

看代碼前,先考慮下,用戶從右向左滑動時,面板應該如何變化;取其中一行,可能性為:

0 0 0 2 -> 2 0 0 0

2 0 4 0 -> 2 4 0 0 

2 2 4 0 -> 4 4 0 0 

大概就這3中可能;

我們算法是這么做的:

拿2 2 4 0 來說:

1、首先把每行有數字的取出來,臨時存儲下來,即[ 2, 2, 4 ];

2、然后遍歷合并第一個相遇的相同的,即[ 4, 4 ,0 ]

3、然后直接放置到原行,不足補0,即[ 4, 4, 0 ,0 ]; 

中間還有幾個操作:

1、生成一個新的數字,游戲在每次用戶滑動時,可能會生成一個數字;我們的生成策略:如果發生移動或者合并,則生成一個數字;

移動的判斷,拿原數據,即【 2 ,2,4,0】和我們第一步臨時存儲的做比較,一一對比(遍歷臨時表),發現不同,則認為移動了;

合并的判斷,在合并的時候會設置合并的標志位為true;

2、加分,如果發生合并,則加分,分值為合并得到的數字,比如 4,4 -> 8 ,即加8分 ; 也只需要在合并的時候進行相加就行了;

介紹完了,來看我們的代碼:

/**
     * 根據用戶運動,整體進行移動合并值等
     */
    private void action(ACTION action)
    {
        // 行|列
        for (int i = 0; i < mColumn; i++)
        {
            List<Game2048Item> row = new ArrayList<Game2048Item>();
            // 行|列
            for (int j = 0; j < mColumn; j++)
            {
                // 得到下標
                int index = getIndexByAction(action, i, j);

                Game2048Item item = mGame2048Items[index];
                // 記錄不為0的數字
                if (item.getNumber() != 0)
                {
                    row.add(item);
                }
            }

            for (int j = 0; j < mColumn && j < row.size(); j++)
            {
                int index = getIndexByAction(action, i, j);
                Game2048Item item = mGame2048Items[index];

                if (item.getNumber() != row.get(j).getNumber())
                {
                    isMoveHappen = true;
                }
            }

            // 合并相同的
            mergeItem(row);


            // 設置合并后的值
            for (int j = 0; j < mColumn; j++)
            {
                int index = getIndexByAction(action, i, j);
                if (row.size() > j)
                {
                    mGame2048Items[index].setNumber(row.get(j).getNumber());
                } else
                {
                    mGame2048Items[index].setNumber(0);
                }
            }

        }
        generateNum();

    }

大體上是兩層循環,外層循環代碼循環次數,內層有3個for循環;

第一個for循環,對應上述:首先把每行有數字的取出來,臨時存儲下來,即[ 2, 2, 4 ];

第二個for循環,判斷是否發生移動;

// 合并相同的
mergeItem(row); 是去進行合并操作,對應上述:然后遍歷合并第一個相遇的相同的,即[ 4, 4 ,0 ];以及加分和設置合并標志位都在方法中;

第三個for循環:設置合并后的值,對應上述:然后直接放置到原行,不足補0,即[ 4, 4, 0 ,0 ]; 

最后生成數字,方法內部會進行判斷游戲是否結束,是否需要生成數字;

那么先看mergeItem的代碼:

/**
     * 合并相同的Item
     * 
     * @param row
     */
    private void mergeItem(List<Game2048Item> row)
    {
        if (row.size() < 2)
            return;

        for (int j = 0; j < row.size() - 1; j++)
        {
            Game2048Item item1 = row.get(j);
            Game2048Item item2 = row.get(j + 1);

            if (item1.getNumber() == item2.getNumber())
            {
                isMergeHappen = true;

                int val = item1.getNumber() + item2.getNumber();
                item1.setNumber(val);

                // 加分
                mScore += val;
                if (mGame2048Listener != null)
                {
                    mGame2048Listener.onScoreChange(mScore);
                }

                // 向前移動
                for (int k = j + 1; k < row.size() - 1; k++)
                {
                    row.get(k).setNumber(row.get(k + 1).getNumber());
                }

                row.get(row.size() - 1).setNumber(0);
                return;
            }

        }

    }

也比較簡單,循環查找相同的number,發現合并數字,加分;

加分我們設置了一個回調,把分數回調出去:

if (mGame2048Listener != null)
{
mGame2048Listener.onScoreChange(mScore);
}

最后看我們生成數字的代碼:

/**
     * 產生一個數字
     */
    public void generateNum()
    {

        if (checkOver())
        {
            Log.e("TAG", "GAME OVER");
            if (mGame2048Listener != null)
            {
                mGame2048Listener.onGameOver();
            }
            return;
        }

        if (!isFull())
        {
            if (isMoveHappen || isMergeHappen)
            {
                Random random = new Random();
                int next = random.nextInt(16);
                Game2048Item item = mGame2048Items[next];

                while (item.getNumber() != 0)
                {
                    next = random.nextInt(16);
                    item = mGame2048Items[next];
                }

                item.setNumber(Math.random() > 0.75 ? 4 : 2);

                isMergeHappen = isMoveHappen = false;
            }

        }
    }

首先判斷是否結束,如果結束了,依然是回調出去,得讓玩的人知道結束了;

然后判斷當然面板是有木有空的格子,如果沒有,在判斷需要生成新的數字么,需要則隨機生成一個新的2或4;

那么如何判斷是否結束呢?

首先肯定是沒有空格了,然后四個方向上沒有相同的數字就結束了:

/**
     * 檢測當前所有的位置都有數字,且相鄰的沒有相同的數字
     * 
     * @return
     */
    private boolean checkOver()
    {
        // 檢測是否所有位置都有數字
        if (!isFull())
        {
            return false;
        }

        for (int i = 0; i < mColumn; i++)
        {
            for (int j = 0; j < mColumn; j++)
            {

                int index = i * mColumn + j;

                // 當前的Item
                Game2048Item item = mGame2048Items[index];
                // 右邊
                if ((index + 1) % mColumn != 0)
                {
                    Log.e("TAG", "RIGHT");
                    // 右邊的Item
                    Game2048Item itemRight = mGame2048Items[index + 1];
                    if (item.getNumber() == itemRight.getNumber())
                        return false;
                }
                // 下邊
                if ((index + mColumn) < mColumn * mColumn)
                {
                    Log.e("TAG", "DOWN");
                    Game2048Item itemBottom = mGame2048Items[index + mColumn];
                    if (item.getNumber() == itemBottom.getNumber())
                        return false;
                }
                // 左邊
                if (index % mColumn != 0)
                {
                    Log.e("TAG", "LEFT");
                    Game2048Item itemLeft = mGame2048Items[index - 1];
                    if (itemLeft.getNumber() == item.getNumber())
                        return false;
                }
                // 上邊
                if (index + 1 > mColumn)
                {
                    Log.e("TAG", "UP");
                    Game2048Item itemTop = mGame2048Items[index - mColumn];
                    if (item.getNumber() == itemTop.getNumber())
                        return false;
                }

            }

        }

        return true;

    }

/**
     * 是否填滿數字
     * 
     * @return
     */
    private boolean isFull()
    {
        // 檢測是否所有位置都有數字
        for (int i = 0; i < mGame2048Items.length; i++)
        {
            if (mGame2048Items[i].getNumber() == 0)
            {
                return false;
            }
        }
        return true;
    }

到此,我們的代碼介紹完畢~~~完成了我們的Game2048Layout ; 接下來看如何使用呢?

寫游戲的過程很艱辛,但是用起來,看看什么叫so easy ; 當成普通的View即可:

4、實踐

1、布局文件:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >

    <com.zhy.game2048.view.Game2048Layout
        android:id="@+id/id_game2048"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_centerInParent="true"
        android:background="#ffffff"
        android:padding="10dp" >
    </com.zhy.game2048.view.Game2048Layout>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@id/id_game2048"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="20dp"
        android:background="#EEE4DA"
        android:orientation="horizontal" >

        <TextView
            android:id="@+id/id_score"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="4dp"
            android:text="Score: 0"
            android:textColor="#EA7821"
            android:textSize="30sp"
            android:textStyle="bold" />
    </LinearLayout>

</RelativeLayout>

2、MainActivity

package com.zhy.game2048;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.os.Bundle;
import android.widget.TextView;

import com.zhy.game2048.view.Game2048Layout;
import com.zhy.game2048.view.Game2048Layout.OnGame2048Listener;

public class MainActivity extends Activity implements OnGame2048Listener
{
    private Game2048Layout mGame2048Layout;

    private TextView mScore;

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

        mScore = (TextView) findViewById(R.id.id_score);
        mGame2048Layout = (Game2048Layout) findViewById(R.id.id_game2048);
        mGame2048Layout.setOnGame2048Listener(this);
    }

    @Override
    public void onScoreChange(int score)
    {
        mScore.setText("SCORE: " + score);
    }

    @Override
    public void onGameOver()
    {
        new AlertDialog.Builder(this).setTitle("GAME OVER")
                .setMessage("YOU HAVE GOT " + mScore.getText())
                .setPositiveButton("RESTART", new OnClickListener()
                {
                    @Override
                    public void onClick(DialogInterface dialog, int which)
                    {
                        mGame2048Layout.restart();
                    }
                }).setNegativeButton("EXIT", new OnClickListener()
                {

                    @Override
                    public void onClick(DialogInterface dialog, int which)
                    {
                        finish();
                    }
                }).show();
    }

}

很簡單,代碼主要就是設置個接口,當發生加分已經游戲結束時會交給Activity去處理~~~如果喜歡,你可以在一個界面放4個游戲~~~

當然了游戲Item的個數也可以動態設置~~~最后貼一個5*5游戲的截圖~~

 

好了,2048到此結束,拿只筆開始設計,然后根據自定義View的經驗去寫,相信你可以學會不少東西~~~

 

并且我們的View是抽取出來的,其實換成圖片也很簡單~~

今天又看了war3十大經典戰役,獻上war3版,代碼就不貼了,改動也就幾行代碼,貼個截圖,紀念我們曾經的war3~~~:

 

額,咋都弄成5*5了~大家可以把mColumn改為4~~~

 

 

2048源碼點擊下載

 

war3版2048點擊下載

 

 

 

 

---------------------------------------------------------------------------------------------------------

建了一個QQ群,方便大家交流。群號:55032675



----------------------------------------------------------------------------------------------------------

博主部分視頻已經上線,如果你不喜歡枯燥的文本,請猛戳(初錄,期待您的支持):

1、高仿微信5.2.1主界面及消息提醒

2、高仿QQ5.0側滑




 

 

 

 

 

 

 

 

 

 

 

 

 

 

來自: http://blog.csdn.net//lmj623565791/article/details/40020137

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!