Android小游戲 之《數(shù)字華容道》

[toc]

0 背景

最近看《最強(qiáng)大腦》,看到其中的“數(shù)字華容道”這個(gè)小游戲挺有意思,于是萌生了自己寫一個(gè)的想法,正好結(jié)合之前的文章《Android開發(fā)藝術(shù)探索》第4章 View的工作原理 ,順便復(fù)習(xí)一下。
GitHub鏈接:https://github.com/LittleFogCat/Shuzihuarongdao

szhrd

說做就做。
經(jīng)過一夜的粗制濫造,初版已經(jīng)完成,現(xiàn)在復(fù)盤一下詳細(xì)過程。

0.1 游戲介紹

在4x4的方格棋盤中,擺放了115一共十五個(gè)棋子。玩家需要在最短時(shí)間內(nèi),移動(dòng)棋子將115按順序排列好。

1 結(jié)構(gòu)

本文app結(jié)構(gòu)很簡單,分為三個(gè)界面:目錄,游戲,高分榜。分別對應(yīng)的是MenuAcitivity、GameActivity、HighScoreActivity。其中MenuActivity為主界面。

2 定義棋盤和棋子

新建棋盤類BoardView,繼承自ViewGroup。在xml文件中直接加入BoardView即可。
新建棋子類CubeView,繼承自TextView。

1.0 棋子

棋子只包含一個(gè)數(shù)字,所以簡單的繼承自TextView即可。由于我們還需要比對棋子是否在正確的位置,所以我們還需要給每個(gè)棋子加上數(shù)字和位置屬性。

public class CubeView extends android.support.v7.widget.AppCompatTextView {
    // ... 
    private Position mPosition;
    private int mNumber;

    public void setNumber(int n) {
        mNumber = n;
        setText(String.valueOf(n));
    }

    public int getNumber() {
        return mNumber;
    }

    public Position getPosition() {
        return mPosition;
    }

    public void setPosition(Position position) {
        this.mPosition = position;
    }
}

這里,我們定義了一個(gè)類Position,用于描述棋子在棋盤中的位置。

class Position {
    int sizeX; // 總列數(shù)
    int sizeY; // 總行數(shù)
    int x; // 橫坐標(biāo)
    int y; // 縱坐標(biāo)

    public Position() {
    }

    Position(int sizeX, int sizeY) {
        this.sizeX = sizeX;
        this.sizeY = sizeY;
    }

    public Position(int sizeX, int sizeY, int x, int y) {
        this.sizeX = sizeX;
        this.sizeY = sizeY;
        this.x = x;
        this.y = y;
    }

    Position(Position orig) {
        this(orig.sizeX, orig.sizeY, orig.x, orig.y);
    }

    /**
     * 移動(dòng)到下一個(gè)位置
     */
    boolean moveToNextPosition() {
        if (x < sizeX - 1) {
            x++;
        } else if (y < sizeY - 1) {
            x = 0;
            y++;
        } else {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "Position{" +
                "x=" + x +
                ", y=" + y +
                '}';
    }
}

我們參考Android系統(tǒng)屏幕坐標(biāo)系,以棋盤左上角為零點(diǎn),每向右一格橫坐標(biāo)加一,每向下一格縱坐標(biāo)加一。如圖:


坐標(biāo)

接下來,我們開始定義棋盤View:BoardView,這也是這個(gè)游戲的重頭戲。

2.1 棋盤屬性

首先,考慮需要添加哪些屬性。由于時(shí)間關(guān)系,我這里只加入了棋盤尺寸。
在style.xml文件中加入:

    <declare-styleable name="BoardView">
        <attr name="sizeH" format="integer" />
        <attr name="sizeV" format="integer" />
    </declare-styleable>

其中sizeH為棋盤列數(shù),sizeV為棋盤行數(shù)。(默認(rèn)4x4大小,以下文中均以4x4為例)
分別對應(yīng)BoardView的mSizeXmSizeY屬性。

2.2 排列棋子

首先我們新建一個(gè)cube_view.xml,作為單顆棋子的布局。在BoardView的構(gòu)造方法中,我們使用LayoutInflater將總共15顆棋子加載出來,并指定它們的位置,逐一保存在mChildren數(shù)組中。

public class BoardView extends ViewGroup {
    // ...

    private CubeView[] mChildren;

    private void init() {
        mChildSize = mSizeX * mSizeY - 1;
        mChildren = new CubeView[mChildSize];
        Position p = new Position(mSizeX, mSizeY);
        for (int i = 0; i < mChildSize; i++) {
            final CubeView view = (CubeView) LayoutInflater.from(getContext()).inflate(R.layout.cube_view, this, false);
            view.setPosition(new Position(p));
            view.setOnClickListener(v -> moveChildToBlank(view));
            addView(view);
            p.moveToNextPosition();
            mChildren[i] = view;
        }
        mBlankPos = new Position(mSizeX, mSizeY, mSizeX - 1, mSizeY - 1);
    }
}

最后,我們記錄了沒有棋子的空格所在位置mBlankPos。這個(gè)位置很關(guān)鍵,因?yàn)槲覀冎蟮牡牟僮髦卸际菄@這個(gè)空格來的。

measure和layout的過程很簡單,這里由于是自己使用,假定寬高都是定值。因?yàn)橹八械腃ubeView都沒有定義寬高,默認(rèn)是0,所以在onMeasure中,我們使用BoardView的寬除以列數(shù),高除以行數(shù),得到每顆棋子的寬高并給其賦值。這樣處理雖然很粗放,但是只是試玩的話并沒有什么影響。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int w = getMeasuredWidth();
        int h = getMeasuredHeight();
        mChildWidth = w / mSizeX;
        mChildHeight = h / mSizeY;

        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            CubeView v = (CubeView) getChildAt(i);
            if (v == null) {
                continue;
            }
            LayoutParams lp = v.getLayoutParams();
            lp.width = mChildWidth;
            lp.height = mChildHeight;
            v.setLayoutParams(lp);
            v.setTextSize(TypedValue.COMPLEX_UNIT_PX, mChildWidth / 3);
        }
    }

我是按照從左往右、從上往下的方式依次排列棋子,并且沒有考慮棋子的margin屬性,所以onLayout很簡單:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();

        for (int i = 0; i < childCount; i++) {
            CubeView v = (CubeView) getChildAt(i);
            Position p = v.getPosition();
            int left = p.x * mChildWidth;
            int top = p.y * mChildHeight;
            int right = left + mChildWidth;
            int bottom = top + mChildHeight;
            v.layout(left, top, right, bottom);
        }
    }

至此,棋子在棋盤中就已經(jīng)排列好了。

3 生成棋局

一開始的時(shí)候,我考慮的是,生成1~15的不重復(fù)隨機(jī)數(shù),然后依次給CubeView賦值即可。即:

/**
 * 用于生成不重復(fù)的隨機(jī)數(shù)
 *
 * @deprecated 可能會(huì)生成不可解的情況
 */
public class RandomNoRepeat {
    private List<Integer> mRandomArr;

    /**
     * 在一串連續(xù)整數(shù)中取隨機(jī)值
     *
     * @param first 連續(xù)整數(shù)的第一個(gè)
     * @param size  連續(xù)整數(shù)的數(shù)量
     */
    RandomNoRepeat(int first, int size) {
        mRandomArr = new ArrayList<>();
        for (int i = first; i < size + first; i++) {
            mRandomArr.add(i);
        }
        Collections.shuffle(mRandomArr);
    }

    int nextInt() {
        if (mRandomArr == null || mRandomArr.isEmpty()) {
            return 0;
        }
        int i = mRandomArr.get(0);
        mRandomArr.remove(0);
        return i;
    }

}

雖然看起來是能行得通的,但是在實(shí)際的游戲過程中,遇到了非常嚴(yán)重的問題,那就是會(huì)出現(xiàn)無解的死局,也就是說無論如何都不可能解出來的棋局。經(jīng)過網(wǎng)上搜索之后證實(shí)了這個(gè)bug的存在,而且市面上流傳的該類app很多都是有這個(gè)bug的!所以這個(gè)辦法就被廢棄掉了,得想一個(gè)新的方法。
由于必須是按照順序放置然后打亂的棋局才能保證有解,不能隨機(jī)亂放置,所以我就模擬手動(dòng)打亂,寫了一個(gè)新的棋局生成器:

public class BoardGenerator {
    private static final int LEFT = 0;
    private static final int UP = 1;
    private static final int RIGHT = 2;
    private static final int DOWN = 3;

    private int[][] mBoard;
    private int mSizeX;
    private int mSizeY;

    private int mBlankX;
    private int mBlankY;

    /**
     * @param sizeX 列數(shù)
     * @param sizeY 行數(shù)
     */
    public BoardGenerator(int sizeX, int sizeY) {
        mSizeX = sizeX;
        mSizeY = sizeY;
        mBoard = new int[sizeY][sizeX];
        generate();
    }


    public void generate() {
        int totalCount = mSizeX * mSizeY - 1;
        int temp = 1;
        for (int i = 0; i < mSizeY; i++) {
            for (int j = 0; j < mSizeX; j++) {
                mBoard[i][j] = temp;
                temp++;
            }
        }
        mBlankX = mSizeX - 1;
        mBlankY = mSizeY - 1;
        for (int i = 0; i < 10000; i++) {
            moveRandomly();
        }
        while (mBlankX != mSizeX - 1) {
            moveToRight(mBlankY, mBlankX);
            mBlankX++;
        }
        while (mBlankY != mSizeY - 1) {
            moveToDown(mBlankY, mBlankX);
            mBlankY++;
        }

        if (mListener != null) {
            mListener.onGenerated(mBoard);
        }
    }


    private void moveRandomly() {
        int r = RandomUtil.randomInt(0, 4);
        switch (r) {
            case LEFT:
                if (moveToLeft(mBlankY, mBlankX)) {
                    mBlankX--;
                }
                break;
            case UP:
                if (moveToUp(mBlankY, mBlankX)) {
                    mBlankY--;
                }
                break;
            case RIGHT:
                if (moveToRight(mBlankY, mBlankX)) {
                    mBlankX++;
                }
                break;
            case DOWN:
                if (moveToDown(mBlankY, mBlankX)) {
                    mBlankY++;
                }
                break;
        }
    }

    private void exchange(int a1, int b1, int a2, int b2) {
        int temp = mBoard[a1][b1];
        mBoard[a1][b1] = mBoard[a2][b2];
        mBoard[a2][b2] = temp;
    }

    private boolean moveToLeft(int a, int b) {
        if (b > 0) {
            exchange(a, b, a, b - 1);
            return true;
        } else {
            return false;
        }
    }

    private boolean moveToRight(int a, int b) {
        if (b < mSizeX - 1) {
            exchange(a, b, a, b + 1);
            return true;
        } else {
            return false;
        }
    }

    private boolean moveToUp(int a, int b) {
        if (a > 0) {
            exchange(a, b, a - 1, b);
            return true;
        } else {
            return false;
        }
    }

    private boolean moveToDown(int a, int b) {
        if (a < mSizeY - 1) {
            exchange(a, b, a + 1, b);
            return true;
        } else {
            return false;
        }
    }

    private OnGeneratedListener mListener;

    public void setOnGeneratedListener(OnGeneratedListener l) {
        mListener = l;
    }

    public interface OnGeneratedListener {
        void onGenerated(int[][] board);
    }
}

原理很簡單,因?yàn)榭崭竦奈恢檬俏ㄒ坏模敲次覀儼芽崭竦纳舷伦笥宜膫€(gè)棋子隨機(jī)找出一個(gè),與空格互換位置,也就模擬了一次手動(dòng)點(diǎn)擊。當(dāng)點(diǎn)擊的次數(shù)足夠多時(shí)(這里循環(huán)了10000次),就可以看做是已經(jīng)打亂的棋盤了。
最后把生成好的棋盤,保存在一個(gè)二維數(shù)組中即可。

(因?yàn)橛袀€(gè)10000次的循環(huán),我擔(dān)心時(shí)間過長,于是將其放在線程中執(zhí)行,但是后來我覺得自己多此一舉了。)

然后,在BoardView中定義一個(gè)setData方法,來把生成好的棋局裝進(jìn)來:

    public void setData(List<Integer> data) {
        for (int i = 0; i < mChildSize; i++) {
            CubeView child = (CubeView) getChildAt(i);
            child.setNumber(data.get(i));
        }
    }

這樣,就完成了棋局的生成。

4 游戲過程

游戲過程基本是極簡的。
在初始化方法中(2.1),我們給每個(gè)棋子都定義了點(diǎn)擊事件,模擬真實(shí)場景。具體來講,就是當(dāng)我們點(diǎn)擊一個(gè)棋子的時(shí)候:如果棋子在空格周圍,則將棋子移動(dòng)到空格處;反之,則不進(jìn)行任何操作。(如果設(shè)置滑動(dòng)同理)
這樣我們的Position類就派上用場了。
在2.1的init()方法中,我們有這么一句:

        view.setOnClickListener(v -> moveChildToBlank(view));

即是,當(dāng)我們點(diǎn)擊了其中一個(gè)棋子時(shí),會(huì)觸發(fā)moveChildToBlank(view)方法。這個(gè)方法的目的正是上面所說。

    public void moveChildToBlank(CubeView child) {
        Position childPos = child.getPosition();
        Position dstPos = mBlankPos;
        if (childPos.x == dstPos.x && Math.abs(childPos.y - dstPos.y) == 1 ||
                childPos.y == dstPos.y && Math.abs(childPos.x - dstPos.x) == 1) {
            child.setPosition(dstPos);

            child.setX(dstPos.x * mChildWidth);
            child.setY(dstPos.y * mChildHeight);

            mBlankPos = childPos;
            mStepCounter.add();
        }
        checkPosition();
    }

在移動(dòng)棋子之后,我們需要檢查一下是否是正確排列的順序,如果是的話,那么表明游戲完成。

5 高分榜

首先創(chuàng)建HighScore類,包含姓名,用時(shí),步數(shù),時(shí)間。

public class HighScore {
    public long useTime;
    public long time;
    public String name = "匿名";
    public int useStep;
}

高分榜使用SharedPreferences+Gson,將一個(gè)List<HighScore>轉(zhuǎn)換為json形式保存在本地。

最佳成績的記錄是在GameActivity中完成的。流程如下:

  1. 進(jìn)入界面,開始生成棋局,同時(shí)讀取本地高分榜;
  2. 生成棋局完成,開始記錄游戲時(shí)間;
  3. 棋局完成,記錄結(jié)束時(shí)間,計(jì)算游戲用時(shí);
  4. 比對本地最佳成績和本次成績,計(jì)算是否打破記錄及保存;
  5. 如果進(jìn)入最佳成績榜,輸入姓名并保存。

總的來說,邏輯簡單清晰。

6 作弊&后記

自己開發(fā)的自然是需要作弊功能了!暫且不表。

由于只用了一個(gè)晚上完成,所以還很粗糙,很多功能不夠完善,而且也沒做適配和測試,難免會(huì)有bug存在。主要是把思路記錄下來,方便以后自己和他人做個(gè)參考。

數(shù)字華容道GitHub地址:https://github.com/LittleFogCat/Shuzihuarongdao

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 滋養(yǎng)女人的四寶: “鮮花、水、音樂、月光” 女人活出智慧,有品味的同時(shí)一定要“有趣” 清晨正蹲MT,不知怎的突發(fā)奇...
    一滴_ddb5閱讀 279評論 0 0
  • 甘德禮(關(guān)鍵詞——等待詳細(xì)解釋)持續(xù)原創(chuàng)分享第60天,約練第15次。 首先祝所有的老師們節(jié)日快樂!祝你們在接下來的...
    華南帝虎閱讀 349評論 0 0
  • 世界人口60多億。一生有:80*365=29200天,平均每天可以遇到1000個(gè)人左右。 一輩子遇到人的總數(shù):29...
    S超然物外閱讀 433評論 0 0
  • 問路 一日,我出去玩,有一個(gè)人轉(zhuǎn)來轉(zhuǎn)去,迷路了。看見我在路邊玩耍,我就走過去,手摸著他的頭,問:“小朋友,這是什么...
    顧家樂1020閱讀 928評論 7 5

友情鏈接更多精彩內(nèi)容