关闭

Android60分钟搞定《FlappyBird》飞扬的小鸟游戏

标签: 高仿FlappyBird游戏SurfaceView的使用自定义SurfaceView
1050人阅读 评论(2) 收藏 举报
分类:

自定义控件系列,前前后后分享了三四篇了,其实,在自定义控件的道路中,我们只仅仅迈出了一小步,未来的时间我们仍需走在路上,去探索那些未知的自定义领域。今天,是个周末的时刻,少些烦躁的代码,来一起实现一个有趣的类似FlappyBird小游戏吧;对于FlappyBird这个小游戏,相信在看这篇文章的你并不陌生,其富有挑战又有一丝乐趣,让玩者玩的不亦乐乎,曾经风靡一时,虽已经离发行过去了三年多的时间,但仍能激起我们心中那份玩游戏时的跌宕起伏。


先看一下我们最终要实现的效果:





其实就是一个特别简单的动作,点击屏幕时,使小球做上下跳动,进行绕过障碍物,碰到障碍物或者碰到底部,则游戏结束,这个小游戏,总体代码不足400行,比较简单,整个Demo地址为:http://download.csdn.net/detail/ming_147/9731585,来,开始一点点的实现吧。


对于这个GIF动图,初显示时其实就是打开应用所启动的Activity,无非就是设置了几个TextView,这个过于简单,就不再叙述,小球所在的页面就是第二页面时,也是一个Activity,这个Activity是游戏的控制页,其实里面就是自己定义的一个继承于SurfaceView的一个View类。


可能有同志要问来,为什么要继承SurfaceView而不是继承View?相信做过小游戏的同志们都会或多或少的知晓,其实SurfaceView是View的子类,内嵌了用于专门绘制的Surface,可以控制其格式和尺寸,及Surface的绘制位置,并且,SurfaceView提供了一个可见的区域,只有在可见区域内Surface才可见,还有SurfaceView默认是使用双缓冲技术,它支持在子线程中绘制图像,不会阻塞主线程,对于游戏的开发它是再适合不过的。


首先呢自定义一个类继承于SurfaceView,在构造方法中,我们可以做一些初始化工作:


private void init() {
    holder this.getHolder();
    
holder.addCallback(this);
    
mPaint new Paint();
    
mPaint.setColor(Color.WHITE);
    
mPaint.setAntiAlias(true);
    
mPaint.setTextSize(50);
    
mPaint.setStyle(Paint.Style.STROKE);
    
setFocusable(true);
    
setFocusableInTouchMode(true);
    this
.setKeepScreenOn(true);
}


初始化工作需要获取SurfaceHolder, SurfaceHolder提供了访问和控制SurfaceView背后的Surface的相关方法,所以这个一定进行获取,获取SurfaceHolder之后,调取addCallBack()方法,让自定义的类实现SurfaceHolder.Callback接口,其实现的三个方法这里做一个简单描述:


surfaceCreated(SurfaceHolder holder):surface创建的时候调用,一般在该方法中启动绘图的线程。


surfaceChanged(SurfaceHolder holder, int format, int width,int height):surface尺寸发生改变的时候调用,如横竖屏切换。


surfaceDestroyed(SurfaceHolder holder) :surface被销毁的时候调用,如退出游戏画面,一般在该方法中停止绘图线程。


对于初始化中其它方法也做一个简单说明,其实源码中也有相关说明:


mPaint.setAntiAlias(true)方法主要是为画笔设置抗锯齿,这样做的作用就是让图像边缘可以相对清晰一点。


mPaint.setStyle(Paint.Style.STROKE);设置画笔为空心。


setFocusable(true);设置可以获取焦点。


setFocusableInTouchMode(true);设置此视图是否可以在触底模式下接受焦点。


this.setKeepScreenOn(true);控制其屏幕是否应继续。


对于小球,虚线,障碍物,关数,我们尽量在surfaceCreated进行初始化,上面也有说明,我们可以在这个方法里获取屏幕的宽高,另外我们再另起一个线程,因为接下来我们用到很多处理数据,及数据很多的地方,isStartGame这个布尔类型的值用于标示游戏是否结束。


/**
 * surface
创建的时候调用,一般在该方法中启动绘图的线程
 */
@Override
public void surfaceCreated(SurfaceHolder holder) {
    isStartGame true;
    
windowWidth this.getWidth();
    
windowHeight this.getHeight();
    
initGame();
    
thread new Thread(this);
    
thread.start();
}


刚进入游戏页面,小球是不动的,虚线也是静止的,我们可以看下下面的初始化代码;


/**
 * 
游戏开始前进行初始化
 */
private void initGame() {
    
if (gameState == GAME_START) {
        
floorLine[0] = 0;
        
floorLine[1] = windowHeight windowHeight 5;

        
levelText[0] = windowWidth 2;
        
levelText[1] = windowHeight 100;
        
level_value 0;

        
circle[0] = windowWidth 3;
        
circle[1] = windowHeight 2;

        
rectangleList.clear();

        
floorLine_width = dp2px(15);

        
speed = dp2px(3);

        
circle_width = dp2px(10);
        
circle_a = dp2px(2);
        
circle_vUp = -dp2px(16);

        
wall_w = dp2px(45);
        
wall_h = dp2px(100);
        
wall_step wall_w 4;


floorLine就是我定义的一个存储底部虚线xy坐标的一个数组:


/**
 * 
底部虚线xy坐标
 */
private int[] floorLine new int[2];


levelText是存储关数的xy坐标的一个数组,level_value是默认第0关。


/**
 * 
关数坐标
 */
private int[] levelText new int[2];


cirle是存储小球的xy坐标的一个数组:


/**
 * 
球的xy坐标
 */
private int[] circle new int[2];


rectangleList这个集合是用于存储障碍物的坐标集合:


private ArrayList<int[]> removeRectangleList new ArrayList<int[]>();


一切初始化工作完成之后,我们就是绘制小球,虚线,障碍物了,前边我们起了一个线程,这些工作我们就可以放到子线程去实现:


@Override
public void run() {
    while (isStartGame) {
        long start = System.currentTimeMillis();
        
drawGame();
        
startGame();
        long 
end = System.currentTimeMillis();
        try 
{
            if (end - start < 50) {
                Thread.sleep(50 - (end - start));
            
}
        } catch (InterruptedException e) {
            e.printStackTrace();
        
}

    }
}


isStartGame这个值前边说过,用于标示游戏是否结束,如果一直没结束,那么run方法就一直执行,记得在surface销毁时,改为false。


/**
 * surface
被销毁的时候调用,如退出游戏画面,一般在该方法中停止绘图线程。
 */
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
    
isStartGame false;
}


关于绘制,前边几篇自定义控件中对这些绘制方法已经做了很多的例子,主要就使确定位置,这是非常重要的,获取画笔,我们可以new一个,也可以从SurfaceHolder里面获取,关于绘制虚线,其y轴是保持不变的,宽度一定,只是x轴逐渐递增;绘制圆其半径也是一定,x轴坐标固定,只有y轴上下移动,关数和障碍物的绘制,只要位置确定,其绘制也类似,当一切画完之后,记得一定要去调用unlockCanvasAndPost来改变内容。


/**
 * 
绘制小球,虚线,障碍物,关数
 * */
private void drawGame() {

    try {
        canvas holder.lockCanvas();//获取画笔
        
mPaint.setColor(Color.WHITE);
        if 
(canvas != null) {
            canvas.drawColor(Color.BLACK);
            int 
floor_start = floorLine[0];

            
mPaint.setStrokeWidth(20);
            
//绘制虚线
            
while (floor_start < windowWidth) {
                canvas.drawLine(floor_startfloorLine[1]floor_start + floorLine_widthfloorLine[1]mPaint);
                
floor_start += floorLine_width 2;
            
}
            mPaint.setStrokeWidth(1);
            
//绘制圆
            
mPaint.setStyle(Paint.Style.FILL);
            
canvas.drawCircle(circle[0]circle[1]circle_widthmPaint);
            
mPaint.setColor(Color.BLACK);
            
canvas.drawText(""circle[0] - 25circle[1] + 20mPaint);
            
mPaint.setColor(Color.WHITE);
            
//绘制关数
            
canvas.drawText("+ String.valueOf(level_value) + ""levelText[0] - 50levelText[1]mPaint);
            
//绘制阻碍物
            
for (int i = 0i < rectangleList.size()i++) {
                int[] wall = rectangleList.get(i);
                float
[] pts = {
                        wall[0]0wall[0]wall[1],
                        
wall[0]wall[1] + wall_hwall[0]floorLine[1],
                        
wall[0] + wall_w0wall[0] + wall_wwall[1],
                        
wall[0] + wall_wwall[1] + wall_hwall[0] + wall_wfloorLine[1],
                        
wall[0]wall[1]wall[0] + wall_wwall[1],
                        
wall[0]wall[1] + wall_hwall[0] + wall_wwall[1] + wall_h
                
};
                 
canvas.drawLines(ptsmPaint);
            
}

        
}
    } catch (Exception e) {

    } finally {
        if (canvas != null)
            holder.unlockCanvasAndPost(canvas);
    
}
}


这里我把游戏分为来三个状态,分别是,游戏前,游戏中,游戏结束三个状态:


/**
 * 
游戏前
 */
private static final int GAME_START 0;
/**
 * 
游戏中
 */
private static final int GAME_ING 1;
/**
 * 
游戏结束
 */
private static final int GAME_OVER = -1;


当手点击屏幕之后,当小球落至虚线位置,或者碰到障碍物,则游戏结束,返回上一个activity,否则继续。

@Override
public boolean onTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        switch (gameState) {
            case GAME_START:
                gameState GAME_ING;
            case 
GAME_ING:
                circle_v circle_vUp;
                break;
            case 
GAME_OVER:
                if (circle[1] >= floorLine[1] - circle_width) {
                    gameState GAME_START;
                    
initGame();
                
}

                break;
        
}
    }
    return true;

}



/**
 * 
开始游戏
 */
private void startGame() {
    switch (gameState) {
        case GAME_START://游戏开始前

            
break;
        case 
GAME_ING://游戏进行时
            
circle_v += circle_a;
            
circle[1] += circle_v;
            if 
(circle[1] > floorLine[1] - circle_width) {
                circle[1] = floorLine[1] - circle_width;
                
gameState GAME_OVER;//圆掉到了虚线以下
            
}

            //虚线的滚动
            
if (floorLine[0] < -floorLine_width) {
                floorLine[0] += floorLine_width 2;
            
}
            floorLine[0] -= speed;


            
removeRectangleList.clear();
            for 
(int i = 0i < rectangleList.size()i++) {
                int[] wall = rectangleList.get(i);
                
wall[0] -= speed;
                if 
(wall[0] < -wall_w) {
                    removeRectangleList.add(wall);
                
else if (wall[0] - circle_width <= circle[0] && wall[0] + wall_w circle_width >= circle[0]
                        && (circle[1] <= wall[1] + circle_width || circle[1] >= wall[1] + wall_h circle_width)) {
                    gameState GAME_OVER;
                
}

                int pass = wall[0] + wall_w circle_width circle[0];
                if 
(pass < && -pass <= speed) {
                    level_value++;
                
}
            }
            if (removeRectangleList.size() > 0) {
                rectangleList.removeAll(removeRectangleList);
            
}

            move_step += speed;
            if 
(move_step wall_step) {

                //坐标随机产生
                int[] wall = new int[]{windowWidth(int) (Math.random() * (floorLine[1] - wall_h) + 0.5 wall_h)};
                
rectangleList.add(wall);
                
move_step 0;
            
}

            break;
        case 
GAME_OVER:
            if (circle[1] < floorLine[1] - circle_width) {
                circle_v += circle_a;
                
circle[1] += circle_v;
                if 
(circle[1] >= floorLine[1] - circle_width) {
                    circle[1] = floorLine[1] - circle_width;
                
}
            } else {

                      //游戏结束返回上一个activity
              AbnerGameCircleActivity.mAbnerGameCircleActivity.showAchievement(level_value);
                
gameState GAME_START;
                
initGame();
            
}
            break;
    
}
}


60分钟,我想,是完不成的,因为这个Demo花费了一下午的时间,能够顺利的完成这个小游戏Demo,感谢百度翻译,因为有许多SurfaceView类中的方法,都是通过翻译才大概的弄懂它,也特别感谢许多默默无闻中一直奉献的同志们,更多Android文章请扫描评论第一条二维码,关注我的公众账号吧。

2
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:48128次
    • 积分:1197
    • 等级:
    • 排名:千里之外
    • 原创:69篇
    • 转载:2篇
    • 译文:0篇
    • 评论:58条
    更多技术请关注我的微信公众号
    文章分类