Android开发的消消乐游戏
我自己有个小白群,大家有兴趣加一下:622038189
注意:最新版本的代码我已经提交,加入了不少动画效果,优化了相关性能。
有特效版本:https://download.csdn.net/download/qq_20698983/10406847
无特效版本:https://download.csdn.net/download/qq_20698983/10383678
对于之前用积分下载了无特效版本的同学,我表示非常抱歉,因为个人原因上传的代码传错了,如果你有再次需要可直接联系我(q:420298524)或在下方留言
游戏效果图(今天更新成动图,但是看起来卡卡的,我程序很流畅的被弄成了这样):
当然你也可以在这个链接里面下载我的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做过坦克大战,基本不用考虑性能问题,嵌入到安卓里面就更完美了。