Android全集Android开发

Android源码 之《最强大脑》“数字华容道”

2019-02-25  本文已影响0人  littlefogcat

[toc]

0 背景

最近看《最强大脑》,看到其中的“数字华容道”这个小游戏挺有意思,于是萌生了自己写一个的想法,正好结合之前的文章《Android开发艺术探索》第4章 View的工作原理 ,顺便复习一下。
GitHub链接:https://github.com/LittleFogCat/Shuzihuarongdao

szhrd

说做就做。
经过一夜的粗制滥造,初版已经完成,现在复盘一下详细过程。

0.1 游戏介绍

在4x4的方格棋盘中,摆放了115一共十五个棋子。玩家需要在最短时间内,移动棋子将115按顺序排列好。

1 结构

本文app结构很简单,分为三个界面:目录,游戏,高分榜。分别对应的是MenuAcitivity、GameActivity、HighScoreActivity。其中MenuActivity为主界面。

2 定义棋盘和棋子

新建棋盘类BoardView,继承自ViewGroup。在xml文件中直接加入BoardView即可。
新建棋子类CubeView,继承自TextView。

1.0 棋子

棋子只包含一个数字,所以简单的继承自TextView即可。由于我们还需要比对棋子是否在正确的位置,所以我们还需要给每个棋子加上数字和位置属性。

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

这里,我们定义了一个类Position,用于描述棋子在棋盘中的位置。

class Position {
    int sizeX; // 总列数
    int sizeY; // 总行数
    int x; // 横坐标
    int y; // 纵坐标

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

    /**
     * 移动到下一个位置
     */
    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系统屏幕坐标系,以棋盘左上角为零点,每向右一格横坐标加一,每向下一格纵坐标加一。如图:


坐标

接下来,我们开始定义棋盘View:BoardView,这也是这个游戏的重头戏。

2.1 棋盘属性

首先,考虑需要添加哪些属性。由于时间关系,我这里只加入了棋盘尺寸。
在style.xml文件中加入:

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

其中sizeH为棋盘列数,sizeV为棋盘行数。(默认4x4大小,以下文中均以4x4为例)
分别对应BoardView的mSizeXmSizeY属性。

2.2 排列棋子

首先我们新建一个cube_view.xml,作为单颗棋子的布局。在BoardView的构造方法中,我们使用LayoutInflater将总共15颗棋子加载出来,并指定它们的位置,逐一保存在mChildren数组中。

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。这个位置很关键,因为我们之后的的操作中都是围绕这个空格来的。

measure和layout的过程很简单,这里由于是自己使用,假定宽高都是定值。因为之前所有的CubeView都没有定义宽高,默认是0,所以在onMeasure中,我们使用BoardView的宽除以列数,高除以行数,得到每颗棋子的宽高并给其赋值。这样处理虽然很粗放,但是只是试玩的话并没有什么影响。

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

至此,棋子在棋盘中就已经排列好了。

3 生成棋局

一开始的时候,我考虑的是,生成1~15的不重复随机数,然后依次给CubeView赋值即可。即:

/**
 * 用于生成不重复的随机数
 *
 * @deprecated 可能会生成不可解的情况
 */
public class RandomNoRepeat {
    private List<Integer> mRandomArr;

    /**
     * 在一串连续整数中取随机值
     *
     * @param first 连续整数的第一个
     * @param size  连续整数的数量
     */
    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;
    }

}

虽然看起来是能行得通的,但是在实际的游戏过程中,遇到了非常严重的问题,那就是会出现无解的死局,也就是说无论如何都不可能解出来的棋局。经过网上搜索之后证实了这个bug的存在,而且市面上流传的该类app很多都是有这个bug的!所以这个办法就被废弃掉了,得想一个新的方法。
由于必须是按照顺序放置然后打乱的棋局才能保证有解,不能随机乱放置,所以我就模拟手动打乱,写了一个新的棋局生成器:

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 列数
     * @param sizeY 行数
     */
    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);
    }
}

原理很简单,因为空格的位置是唯一的,那么我们把空格的上下左右四个棋子随机找出一个,与空格互换位置,也就模拟了一次手动点击。当点击的次数足够多时(这里循环了10000次),就可以看做是已经打乱的棋盘了。
最后把生成好的棋盘,保存在一个二维数组中即可。

(因为有个10000次的循环,我担心时间过长,于是将其放在线程中执行,但是后来我觉得自己多此一举了。)

然后,在BoardView中定义一个setData方法,来把生成好的棋局装进来:

    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),我们给每个棋子都定义了点击事件,模拟真实场景。具体来讲,就是当我们点击一个棋子的时候:如果棋子在空格周围,则将棋子移动到空格处;反之,则不进行任何操作。(如果设置滑动同理)
这样我们的Position类就派上用场了。
在2.1的init()方法中,我们有这么一句:

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

即是,当我们点击了其中一个棋子时,会触发moveChildToBlank(view)方法。这个方法的目的正是上面所说。

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

在移动棋子之后,我们需要检查一下是否是正确排列的顺序,如果是的话,那么表明游戏完成。

5 高分榜

首先创建HighScore类,包含姓名,用时,步数,时间。

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

高分榜使用SharedPreferences+Gson,将一个List<HighScore>转换为json形式保存在本地。

最佳成绩的记录是在GameActivity中完成的。流程如下:

  1. 进入界面,开始生成棋局,同时读取本地高分榜;
  2. 生成棋局完成,开始记录游戏时间;
  3. 棋局完成,记录结束时间,计算游戏用时;
  4. 比对本地最佳成绩和本次成绩,计算是否打破记录及保存;
  5. 如果进入最佳成绩榜,输入姓名并保存。

总的来说,逻辑简单清晰。

6 作弊&后记

自己开发的自然是需要作弊功能了!暂且不表。

由于只用了一个晚上完成,所以还很粗糙,很多功能不够完善,而且也没做适配和测试,难免会有bug存在。主要是把思路记录下来,方便以后自己和他人做个参考。

数字华容道GitHub地址:https://github.com/LittleFogCat/Shuzihuarongdao

上一篇 下一篇

猜你喜欢

热点阅读