Android项目实战 -- 2048小游戏

每学会一点知识,都要加以实战巩固,否则都很容易忘记。

这次我给大家带来一个简单的2048小游戏项目实战练习,首先来看看项目的最终运行效果,如果觉得合你的胃口,就继续往下看,不然的话就可以提前溜咯:

1.png

2.png
3.png

代码解构如下:
代码解构.PNG

都有哪些功能?

  • 基本的4×4格子
  • 5×5、6×6格子扩展
  • 游戏模式:无限模式(就是没有了2048的上限,可以一直玩下去)
  • 游戏进度保存(为了避免体积增大,使用的是原生的Sqlite)
  • 外挂模式(手势识别GestureOverlayView)

本项目是用Java编写,本来是想用Kotlin写的,但这个其实是很久以前写的,只是功能还不完善,这次给他完善了下,由于之前使用Java写的,所有我就懒得改了。🙂

本项目已申请上线酷安平台,等通过审核后我把链接放出来,有兴趣的可以直接下载运行看效果。
已上线,点击下载体验😍

实现思想是参看徐宜生的《Android群英传》,并做了些许改变
《Android群英传》

确定布局

GameView
游戏的面板,即4x4的格子面板。它是本实例实现的关键。要实现这个布局,方法有很多,例如自定义一个View, 这个是万能方法,但是需要计算各个小方块的坐标,比较复杂。再比如用GridView,但是却不太好控制空格的小方块。因此,笔者最后选用了GridLayout 布局,这个布局是Android 4.0新增的布局。该布局的引人,极大地方便了Grid类型的布局开发,不熟悉该布局的读者朋友可以在Android开发者网站上找到相关的开发资料。

Cell
游戏中移动的小方块是2048最小的游戏对象。通过面向对象的设计方法,可以将这些小方块抽象成-个个对象。小方块的颜色、显示数字等属性都在对象中进行设置。对方块的合并、产生等操作,也是基于对象的操作,这样非常有利于程序逻辑的控制。

2048算法思路

玩家在进行上、下、左、右地滑动时,先去判断每行(列), 使用0来代表空格,如果某一行(列)的数字为2204,那么首先将这- -行(列)的非0数字存人-一个list, 即224。接下来,根据游戏规则,将2和2进行合并,即44。并将其作为该行(列)的返回值,从滑动的方向开始放置list中的数字。这样将每行(列)处理完毕后,就完成了-一次滑动。

GameView

继承自GridLayout,作为整个游戏的界面。

// 2048界面
public class GameView extends GridLayout {
...
}

再定义一个类Cell来表示每一个格子:

// 小格子
public class Cell extends FrameLayout {

    /**
     * 显示数字的TextView
     */
    private TextView cellShowText;

    /**
     * 显示的数字
     */
    private int digital;

    public Cell(Context context) {
        super(context);
    }

    public Cell(@NonNull Context context, int leftMargin, int topMargin, int bottomMargin) {
        super(context);
        init(context, leftMargin, topMargin, bottomMargin);
    }

    /**
     * 初始化
     */
    private void init(@NonNull Context context, int leftMargin, int topMargin, int bottomMargin) {
        ...
        LayoutParams params = new LayoutParams(-1, -1);
        params.setMargins(leftMargin, topMargin, 0, bottomMargin);
        addView(cellShowText, params);
        setDigital(0);
    }

    ...

    /**
     * 设置数字
     */
    public void setDigital(int digital) {
        this.digital = digital;
        cellShowText.setBackgroundResource(getBackgroundResource(digital));
        if (digital <= 0) {
            cellShowText.setText("");
        } else {
            cellShowText.setText(String.valueOf(digital));
        }
    }
    ...
}

游戏初始化需要根据难度向GameView添加所有的Cell

    /**
     * 初始化向布局中添加空格子
     *
     * @param cellWidth  格子宽
     * @param cellHeight 格子高
     */
    private void addCell(int cellWidth, int cellHeight) {
        Cell cell;
        for (int i = 0; i < gridColumnCount; i++) {
            for (int j = 0; j < gridColumnCount; j++) {
                if (i == gridColumnCount - 1) {
                    // 为最底下的格子加上bottomMargin
                    cell = new Cell(getContext(), 16, 16, 16);
                } else {
                    cell = new Cell(getContext(), 16, 16, 0);
                }
                cell.setDigital(0);
                addView(cell, cellWidth, cellHeight);
                cells[i][j] = cell;
            }
        }
    }

获取所有的空格子,也就是还没有数字的格子:

    /**
     * 获取空格子
     */
    private void getEmptyCell() {
        // 清空
        emptyCellPoint.clear();
        // 遍历所有格子,记录所有空格子的坐标位置
        for (int i = 0; i < gridColumnCount; i++) {
            for (int j = 0; j < gridColumnCount; j++) {
                // 空格子
                if (cells[i][j].getDigital() <= 0) {
                    emptyCellPoint.add(new Point(i, j));
                }
            }
        }
    }

添加一个数字:

    /**
     * 添加随机数字(2或4)或直接添加一个1024
     *
     * @param isCheat 是否是开挂
     */
    public void addDigital(boolean isCheat) {
        getEmptyCell();
        if (emptyCellPoint.size() > 0) {
            // 随机取出一个空格子的坐标位置
            Point point = emptyCellPoint.get((int) (Math.random() * emptyCellPoint.size()));
            if (isCheat) {
                cells[point.x][point.y].setDigital(1024);
            } else {
                // 通过坐标位置获取到此空格子并以4:6的概率随机设置一个2或4
                cells[point.x][point.y].setDigital(Math.random() > 0.4 ? 2 : 4);
            }
            // 设置动画
            setAppearAnim(cells[point.x][point.y]);
        }
    }

右滑处理(其他几个方向的滑动我就不贴出来了,需要看的可以到我的Github上看源码):

private void swipeRight() {
        // 判断是否需要添加数字
        boolean needAddDigital = false;
        for (int i = gridColumnCount - 1; i >= 0; i--) {
            for (int j = gridColumnCount - 1; j >= 0; j--) {
                // 获取当前位置数字
                int currentDigital = cells[i][j].getDigital();
                someData.add(currentDigital);
                if (currentDigital != 0) {
                    // 记录数字
                    if (recordPreviousDigital == -1) {
                        recordPreviousDigital = currentDigital;
                    } else {
                        // 记录的之前的数字和当前数字不同
                        if (recordPreviousDigital != currentDigital) {
                            // 加入记录的数字
                            dataAfterSwipe.add(recordPreviousDigital);
                            recordPreviousDigital = currentDigital;
                        } else {// 记录的之前的数字和当前的数字相同
                            // 加入*2
                            dataAfterSwipe.add(recordPreviousDigital * 2);
                            // 记录得分
                            recordScore(recordPreviousDigital * 2);
                            // 重置记录数字
                            recordPreviousDigital = -1;
                        }
                    }
                }
            }

            if (recordPreviousDigital != -1) {
                dataAfterSwipe.add(recordPreviousDigital);
            }

            // 补0
            int temp = gridColumnCount - dataAfterSwipe.size();
            for (int k = 0; k < temp; k++) {
                dataAfterSwipe.add(0);
            }
            Collections.reverse(dataAfterSwipe);
            // 若原始数据和移动后的数据不同,视为界面发生改变
            Collections.reverse(someData);
            if (!someData.equals(dataAfterSwipe)) {
                needAddDigital = true;
            }
            someData.clear();

            // 重新设置格子数据
            int index = 0;
            for (int p = 0; p < gridColumnCount; p++) {
                cells[i][p].setDigital(dataAfterSwipe.get(index++));
            }
            // 重置数据
            recordPreviousDigital = -1;
            dataAfterSwipe.clear();
        }
        if (needAddDigital) {
            // 添加一个随机数字(2或4)
            addDigital(false);
            playSound();
        }
        judgeOverOrAccomplish();
    }

辅助方法:判断滑动的方向:

    /**
     * 获取滑动方向<br />
     * 注:先依据在轴上滑动距离的大小,判断在哪个轴上滑动
     *
     * @param offsetX 在X轴上的移动距离
     * @param offsetY 在Y轴上的移动距离
     * @return 滑动方向
     * <br />
     * 注:0右滑、1左滑、2下滑、3上滑、-1未构成滑动
     */
    private int getOrientation(float offsetX, float offsetY) {
        // X轴移动
        if (Math.abs(offsetX) > Math.abs(offsetY)) {
            if (offsetX > MIN_DIS) {
                return 0;
            } else if (offsetX < -MIN_DIS) {
                return 1;
            } else {
                return -1;
            }
        } else {// Y轴移动
            if (offsetY > MIN_DIS) {
                return 2;
            } else if (offsetY < -MIN_DIS) {
                return 3;
            } else {
                return -1;
            }
        }
    }

保存游戏进度

说一下思路:
首先定义一个实体类CellEntity,作为保存到数据库中的一个格子实体,代表游戏中某个位置的数据,然后通过遍历所有的格子,找出其中有数字的,并记录下每个格子的位置以及上面的数字作为一个实体类CellEntity,最后全部找完后通过ContentValues一个个的插入到数据库中就完事啦。下次进入APP时先去数据库中读,有数据的话就拿出保存的数据添加到界面中,否则就初始化游戏随机添加两个格子。 我的处理是每10S自动保存一次数据,然后用户主动退出时再保存一次数据。
就像这样:

private void startGame() {
        // 将所有的格子重置
        reset();
        ArrayList<CellEntity> data = new ArrayList<>();
        // 准备到数据库中读
        SQLiteDatabase db = gameDatabaseHelper.getWritableDatabase();
        Cursor cursor = db.query(Config.getTableName(), null, null, null,
                null, null, null);
        if (null != cursor) {
            if (cursor.moveToFirst()) {
                do {
                    int x = cursor.getInt(cursor.getColumnIndex("x"));
                    int y = cursor.getInt(cursor.getColumnIndex("y"));
                    int num = cursor.getInt(cursor.getColumnIndex("num"));
                    data.add(new CellEntity(x, y, num));
                } while (cursor.moveToNext());
            }
            cursor.close();
        }

        if (data.size() <= 2) {
            // 初始化游戏,随机添加两个格子到界面中
            initGame();
        } else {
            // 恢复上次的游戏进度,将保存的数据添加到界面中
            resumeGame(data);
        }
    }

总结:

基本上就是这样了,代码的话不算太多,给新手做练手项目吧!GitHub传送门
涉及到的知识点汇总:

  • Sqlite
  • SharedPreferences
  • GestureOverlayView
  • Animation
  • Spannable
  • Handler
  • BroadcastReceiver
  • Timer
  • 自定义View

都是Android原生的东西,还是非常有学习价值的,没有用到任何第三方插件(本来想用RxJava的,想想算了,不能用任何库!😊)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,367评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,959评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,750评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,226评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,252评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,975评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,592评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,497评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,027评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,147评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,274评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,953评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,623评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,143评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,260评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,607评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,271评论 2 358

推荐阅读更多精彩内容