欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android开发的消消乐游戏

程序员文章站 2022-05-29 21:02:36
...
我自己有个小白群,大家有兴趣加一下:622038189

注意:最新版本的代码我已经提交,加入了不少动画效果,优化了相关性能。
有特效版本:https://download.csdn.net/download/qq_20698983/10406847
无特效版本:https://download.csdn.net/download/qq_20698983/10383678

对于之前用积分下载了无特效版本的同学,我表示非常抱歉,因为个人原因上传的代码传错了,如果你有再次需要可直接联系我(q:420298524)或在下方留言

游戏效果图(今天更新成动图,但是看起来卡卡的,我程序很流畅的被弄成了这样):
Android开发的消消乐游戏

当然你也可以在这个链接里面下载我的apk试玩
apk要求:Android version 6.0
链接:https://pan.baidu.com/s/1eTkO9mq 密码:w48l

设计思路

一、实体类,封装一个动物头像。包含x,y坐标,图片(bitmap),一个id用于匹配,宽、高
二、布局方面一个Activity和一个自定义的View,主角当然是我们这个View。
三、在构造方法里面初始化游戏相关数据

public GameView(Context context, AttributeSet attr) {
        super(context, attr);
        screenHeight = DisplayUtil.getScreenHeight(context);
        screenWidth = DisplayUtil.getScreenWidth(context);
        // 音乐相关初始化
        bgMedia = MediaPlayer.create(this.getContext(), R.raw.bg_game);
        bgMedia.setOnCompletionListener(this);
        bgMedia.start();
        // swap = MediaPlayer.create(this.getContext(), R.raw.swap);

        // int ave = screenWidth / (row + 2); // 将屏幕宽度分为 row + 2 等份
        int ave = 0;
        int size = (screenWidth - ave * 2) / row; // 其它两分为:舞台距离屏幕左右边的像素
        ZooUtil.initZooData(size, size, this.getResources()); // 初始化动物头像数据
        // 背景图片
        background = BitmapFactory.decodeResource(this.getResources(), R.mipmap.game_bg);
        floorBg = BitmapFactory.decodeResource(this.getResources(), R.mipmap.floor_bg);
        /*
         * 计算出舞台距离左边屏幕的距离
         * 计算方式为:
         *   (屏幕总宽度 - 人物头像的宽 * 总行数) / 2
         */
        int leftSpan = (screenWidth - ZooUtil.getAnimalWidth() * row) / 2;
        int topSpan = (screenHeight - ZooUtil.getAnimalHeight() * col) / 3;
        // 将游戏舞台的坐标、高宽保存起来
        StageUtil.initStage(leftSpan, topSpan,
                leftSpan + ZooUtil.getAnimalWidth() * row,
                topSpan + ZooUtil.getAnimalHeight() * col);
        // 实例化画笔
        paint = new Paint();
        paint.setFlags(Paint.ANTI_ALIAS_FLAG);
        paint.setAntiAlias(true); // 消除锯齿
        initGamePoint();
    }

    /**
     * 生成游戏坐标
     */
    private void initGamePoint() {
        currScore = 0; // 清空当前得分
        bitmaps = new FlashBitmap[row][col];
        // 生成背景图片的坐标(仅背景图片,后续可考虑将特效也加进来)
        for (int i = 0; i < row; ++i) {
            for (int j = 0; j < col; ++j) {
                do {
                    // 计算头像坐标
                    FlashBitmap bitmap = ZooUtil.getAnimal();
                    bitmap.setX(StageUtil.getStage().getX() + i * ZooUtil.getAnimalWidth());
                    bitmap.setY(StageUtil.getStage().getY() + j * ZooUtil.getAnimalHeight());
                    transBitmap[i][j] = bitmap.clone();
                    bitmap.setY(0); // 在顶部慢慢下落
                    bitmaps[i][j] = bitmap;
                } while(StageUtil.checkClearPoint(bitmaps));
            }
        }
    }

四、重写该View的onDraw方法,在这个方法里面进行动物的绘制

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 绘制背景图片
        Bitmap bgBitmap = DisplayUtil.resizeBitmap(background, screenWidth, screenHeight);
        canvas.drawBitmap(bgBitmap, 0, 0, paint);
        Bitmap floor = DisplayUtil.resizeBitmap(floorBg, ZooUtil.getAnimalWidth(), ZooUtil.getAnimalHeight());
        // 每一个小头像背后的背景
        for (int i = 0; i < row; ++i) {
            for (int j = 0; j < col; ++j) {
                if(transBitmap[i][j] != null){
                    int x = (int) transBitmap[i][j].getX();
                    int y = (int) transBitmap[i][j].getY();
                    canvas.drawBitmap(floor, x, y, paint);
                }
            }
        }
        // 舞台中的所有动物头像
        FlashBitmap bitmap;
        for (int i = 0; i < row; ++i) {
            for (int j = 0; j < col; ++j) {
                bitmap = bitmaps[i][j];
                if(bitmap != null // 不为空并且坐标点要进入舞台
                        && StageUtil.inStage(bitmap.getX(), bitmap.getY() + bitmap.getHeight() / 2)){
                    canvas.drawBitmap(bitmap.getBitmap(), bitmap.getX(), bitmap.getY(), paint);
                }
            }
        }
        // 是否需要加载消除
        synchronized (this){
            if (load) {
                pool.execute(new Runnable() {
                    @Override
                    public void run() {
                        clearBitmap();
                    }
                });
                load = false;
            }
        }
        paint.setColor(Color.WHITE);
        paint.setTextSize(32);
        paint.setTypeface(Typeface.create(Typeface.DEFAULT_BOLD , Typeface.BOLD));
        canvas.drawText("当前关卡:" + level, 10, StageUtil.getStage().getHeight() + 70, paint);
        canvas.drawText("当前得分:" + currScore, 10, StageUtil.getStage().getHeight() + 120, paint);
        canvas.drawText("通关分数:" + accessScore[level - 1], 10, StageUtil.getStage().getHeight() + 170, paint);
        // 刷新屏幕的频率(理论上小于25,人就会感觉物体是在移动)
        postInvalidateDelayed(1);
    }

五、重写onTouchEvent方法监听该View的按下、抬起、移动等相关事件

@Override
    public boolean onTouchEvent(MotionEvent event) {
        // 判断交换状态是否完毕
        if(swapState){
            return false;
        }
        // 如果正在做下落动画不允许操作
        if(loadAnimalState){
            return false;
        }
        // 获取当前触控位置
        float ex = event.getX();
        float ey = event.getY();
        switch (event.getAction()) {
            // 按下
            case MotionEvent.ACTION_DOWN:
                // 判断是否该点是按在舞台上
                if (!isDown && StageUtil.inStage(ex, ey)) {
                    p1.setX(ex);
                    p1.setY(ey);
                    isDown = true;
                }
                break;
            // 移动
            case MotionEvent.ACTION_MOVE:
                // 判断是否该点是按在舞台上
                if (!isDown && StageUtil.inStage(ex, ey)) {
                    p1.setX(ex);
                    p1.setY(ey);
                    isDown = true;
                }
                break;
            // 抬起
            case MotionEvent.ACTION_UP:
                if (isDown) {
                    p2.setX(ex);
                    p2.setY(ey);
                    isDown = false;
                    prepSwap(); // 预处理交换
                }
                break;
        }
        // 使系统响应事件,返回true
        return true;
    }

基本上到了这里,这个游戏的主体就已经完成了。
下面主要介绍交换和下落以及如何制作交换和下落的动画

1:交换

我的整个游戏舞台都是用数组保存的,所以交换直接两个坐标交换一下就算完成了

    // 两个方块的交换状态
    private boolean swapState = false;
    // 载入动物头像动画是否结束状态
    private boolean loadAnimalState = false;
    // 是否要加载舞台消除动画(程序运行时立即加载)
    private boolean load = true;
    // 虚拟背景
    private FlashBitmap[][] transBitmap = new FlashBitmap[row][col];
    // 动物头像以及游戏坐标
    private FlashBitmap[][] bitmaps = new FlashBitmap[row][col];
    // 背景音乐
    private MediaPlayer bgMedia; // , clearMedia, swap;
    // 线程池
    ExecutorService pool = Executors.newFixedThreadPool(5);
//    private BlockingQueue queue = new LinkedBlockingQueue();
//    private ThreadPoolExecutor pool = new ThreadPoolExecutor(3,
//            10,
//            10,
//            TimeUnit.SECONDS,
//            queue);

动画效果的思路如下:

1.1:定义两个线程,分别计算出两个头像要交换到的终点,这个终点是通过计算出来的
例如:a[0][0] -> a[0][1], 那么a[0][0]肯定是向右,a[0][1]肯定是向左,只要稍加判断就可以得出a[0][0]是x轴到达 a[0][1],这里大家不要误解,我的真实坐标是一个数组,但是画在地图上的坐标是这个数组所对应的x,y ,所以交换的时候先让真实数组交换,然后再根据真实数组的坐标,计算出x或y需要到达的位置.
a[0][1]所对应的 x 的真实坐标应为:0 * 动物宽度 + 舞台左边界距离(0是数组里面的)
a[0][1]所对应的 y 的真实坐标应为:1 * 动物高度 + 舞台上边界距离(1是数组里面的)
1.2:计算出这两个值,判断一下大小,然后定义两个线程同时启动就ok了,因为是两个线程不停的交互移动,所以肉眼是感觉不出来的
这是我的swap方法里面计算交换后x, y位置的代码

// 判断是横着交换还是竖的交换
final int px1 = (int) StageUtil.getStage().getX() + x1 * ZooUtil.getAnimalWidth();
final int py1 = (int) StageUtil.getStage().getY() + y1 * ZooUtil.getAnimalHeight();
final int px2 = (int) StageUtil.getStage().getX() + x2 * ZooUtil.getAnimalWidth();
final int py2 = (int) StageUtil.getStage().getY() + y2 * ZooUtil.getAnimalHeight();
// 取到交换后的两个坐标对象
final FlashBitmap one = bitmaps[x1][y1];
final FlashBitmap two = bitmaps[x2][y2];

2:动物下落

下落我之前设想一个思路,是用多线程实现,但是大部分内存杀手估计就是像我这种的程序员吧,当然用多线程那肯定是方便多了,消除几个后,直接把每个要下落的动物都建立一个线程,慢慢的加这个动物的y轴,只要判断一下这个动物头像的最后落点在哪里(计算方式同swap里面的),当然要考虑一下动物头像的下落坐标,不要超过它的下面那个动物头像,因为线程,有的快,有的慢, 这个是没法保证的。
这种方案完成后,我找群里的人帮我测试了一下,大部分人都说反应慢,效果不好,动画不流畅。之后脑子里一直想着用一个线程处理掉这个下落的动画,而下落又是好多头像同时进行的,继而我又想起如何在每个动物之间快速切换,最后想了很久,想到了一种方案,两个for循环控制着所有的动物头像,每次循环只将一个动物的y轴加一步,然后就结束,在两个for之外再加一个while循环,这个while检测到所有的动物头像都到达终点后才退出,这样的话,用一个线程达到了我想要的效果。
代码片断:

// 移动其它头像
boolean updateFlag = false;
boolean canDoWhile;
int[] index = new int[col];
do {
    canDoWhile = false;
    // 开始进行下落处理
    for(int j = col - 1; j >= 0; --j){
        for(int i = 0; i < row; ++i){
            if(bitmaps[i][j] != null){
                if(!moveStageAnimal(i, j)){
                    canDoWhile = true;
                }
            } else {
                // 只要检测到有任意一个空位就要进行更新
                updateFlag = true;
                // 如果是空,检测从当前位置往上是否还有其它动物头像
                boolean hasPoint = false;
                for(int k = j - 1; k >= 0; --k){
                    // 如果检测到有一个点的话就不搞事情了
                    if(bitmaps[i][k] != null){
                        hasPoint = true;
                    }
                }
                // 如果没有就直接生成一个新点在这个位置
                // 真实坐标是这个位置,但显示在地图上的坐标要给个到 (x, 0)
                if(!hasPoint) {
                    FlashBitmap bitmap = ZooUtil.getAnimal();
                    bitmap.setX(StageUtil.getStage().getX() + i * ZooUtil.getAnimalWidth());
                    bitmap.setY(StageUtil.getStage().getY() - (index[i] + 1) * ZooUtil.getAnimalHeight());
                    bitmaps[i][j] = bitmap;
                    index[i]++;
                }
            }
        }
    }
    // 动画停留间隔
    DisplayUtil.sleep(time);
} while(canDoWhile);

最后再看一个下落的移动动物头像方法

/**
 * 移动舞台动物
 * @param x x
 * @param y y
 * @return 成功true 失败false
 */
private boolean moveStageAnimal(int x, int y) {
    // 记录当前的点
    int j;
    float currY = bitmaps[x][y].getY();
    // 寻找最佳底部的空位
    for(j = col - 1; j >= 0; --j){
        // 不能小于以前的位置
        if(j <= y){
            break;
        }
        // 从底往上找,找到的第一个空位就为要到的位置
        if(bitmaps[x][j] == null){
            break;
        }
    }
    // 有最新点时才进行交换
    if(j != y){
        FlashBitmap temp = bitmaps[x][y];
        bitmaps[x][y] = null;
        bitmaps[x][j] = temp;
    }
    // 不允许在这之上的方块提前下落到下一个方块后面
    if(j < col - 1 && bitmaps[x][j + 1] != null){
        if(currY + ZooUtil.getAnimalHeight() >= bitmaps[x][j + 1].getY()){
            return true;
        }
    }
    // 到达指定高度停止
    if(currY >= j * ZooUtil.getAnimalHeight() + StageUtil.getStage().getY()){
        return true;
    }
    // 大于舞台高度直接停止
    if(currY + bitmaps[x][j].getHeight() >= StageUtil.getStage().getHeight()){
        return true;
    }
    // 自增
    currY += speed;
    bitmaps[x][j].setY(currY);
    return false;
}

做这个游戏大概花了两天半的时间,主要是为了更深刻的认识多线程,另外也是听说安卓6.0变动很大,所以用安卓做,其实安卓里面嵌入H5也比较合适的,我用h5做过坦克大战,基本不用考虑性能问题,嵌入到安卓里面就更完美了。