2016年9月7日 星期三

Android SurfaceView实战 带你玩转flabby bird (上)

Ref : http://blog.csdn.net/lmj623565791/article/details/42965779

1、概述

哈,记得以前写过Android SurfaceView实战 打造抽奖转盘 , 同属于SurfaceView系列,基本可以从这篇博文中学习到SurfaceView的用法,以及利用SurfaceView做抽奖转盘。但是其中缺少一部分的知识点,就是与用户交互时界面的改变,所以今天给大家再带来本篇博文教大家如何做flabby bird这款游戏,这游戏虽然不难,但是也为其作者挣了不少钱,大家在学会以后,可以尽可能发挥自己的创意,做属于自己的游戏,说不定下一个火的奏是你。
ok,那么首先上下效果图:



由于上传图片最大限制为2M,所以做了压缩处理,凑合看吧 ~~~

2、分析

仔细观察游戏,需要绘制的有:背景、地板、鸟、管道、分数;
游戏开始时:
地板给人一种想左移动的感觉;
管道与地板同样的速度向左移动;
鸟默认下落;
当用户touch屏幕时,鸟上升一段距离后,下落;
运动过程中需要判断管道和鸟之间的位置关系,是否触碰,是否穿过等,需要计算分数。
好了,大概就这么多,那我们首先开始考虑绘制~~~

3、SurfaceView的一般写法

接下来,我们首先编写下SurfaceView的一般写法:
  1. package com.zhy.view;  
  2.   
  3. import android.content.Context;  
  4. import android.graphics.Canvas;  
  5. import android.graphics.PixelFormat;  
  6. import android.util.AttributeSet;  
  7. import android.view.SurfaceHolder;  
  8. import android.view.SurfaceHolder.Callback;  
  9. import android.view.SurfaceView;  
  10.   
  11. public class GameFlabbyBird extends SurfaceView implements Callback, Runnable  
  12. {  
  13.   
  14.     private SurfaceHolder mHolder;  
  15.     /** 
  16.      * 与SurfaceHolder绑定的Canvas 
  17.      */  
  18.     private Canvas mCanvas;  
  19.     /** 
  20.      * 用于绘制的线程 
  21.      */  
  22.     private Thread t;  
  23.     /** 
  24.      * 线程的控制开关 
  25.      */  
  26.     private boolean isRunning;  
  27.   
  28.     public GameFlabbyBird(Context context)  
  29.     {  
  30.         this(context, null);  
  31.     }  
  32.   
  33.     public GameFlabbyBird(Context context, AttributeSet attrs)  
  34.     {  
  35.         super(context, attrs);  
  36.   
  37.         mHolder = getHolder();  
  38.         mHolder.addCallback(this);  
  39.   
  40.         setZOrderOnTop(true);// 设置画布 背景透明  
  41.         mHolder.setFormat(PixelFormat.TRANSLUCENT);  
  42.   
  43.         // 设置可获得焦点  
  44.         setFocusable(true);  
  45.         setFocusableInTouchMode(true);  
  46.         // 设置常亮  
  47.         this.setKeepScreenOn(true);  
  48.   
  49.     }  
  50.   
  51.     @Override  
  52.     public void surfaceCreated(SurfaceHolder holder)  
  53.     {  
  54.   
  55.         // 开启线程  
  56.         isRunning = true;  
  57.         t = new Thread(this);  
  58.         t.start();  
  59.     }  
  60.   
  61.     @Override  
  62.     public void surfaceChanged(SurfaceHolder holder, int format, int width,  
  63.             int height)  
  64.     {  
  65.         // TODO Auto-generated method stub  
  66.   
  67.     }  
  68.   
  69.     @Override  
  70.     public void surfaceDestroyed(SurfaceHolder holder)  
  71.     {  
  72.         // 通知关闭线程  
  73.         isRunning = false;  
  74.     }  
  75.   
  76.     @Override  
  77.     public void run()  
  78.     {  
  79.         while (isRunning)  
  80.         {  
  81.             long start = System.currentTimeMillis();  
  82.             draw();  
  83.             long end = System.currentTimeMillis();  
  84.   
  85.             try  
  86.             {  
  87.                 if (end - start < 50)  
  88.                 {  
  89.                     Thread.sleep(50 - (end - start));  
  90.                 }  
  91.             } catch (InterruptedException e)  
  92.             {  
  93.                 e.printStackTrace();  
  94.             }  
  95.   
  96.         }  
  97.   
  98.     }  
  99.   
  100.     private void draw()  
  101.     {  
  102.         try  
  103.         {  
  104.             // 获得canvas  
  105.             mCanvas = mHolder.lockCanvas();  
  106.             if (mCanvas != null)  
  107.             {  
  108.                 // drawSomething..  
  109.             }  
  110.         } catch (Exception e)  
  111.         {  
  112.         } finally  
  113.         {  
  114.             if (mCanvas != null)  
  115.                 mHolder.unlockCanvasAndPost(mCanvas);  
  116.         }  
  117.     }  
  118. }  

这个基础的类,在Android SurfaceView实战 打造抽奖转盘已经出现过,就不多说了,大家以后写SurfaceView的相关程序,可以直接拷贝,在此类基础上编写。

4、绘制


1、绘制背景

最简单的当然是背景了,直接drawBitmap即可。
我们添加需要的成员变量,以及初始化一些参数,然后添加drawBg方法,最后在draw中调用drawBg;

2、绘制bird

鸟在我们的屏幕中,初始化时需要一个位置,x上,肯定是居中,y上我们取2/3的高度;
关于bird,我们单独创建一个类:
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. package com.zhy.view;  
  2.   
  3. import android.content.Context;  
  4. import android.graphics.Bitmap;  
  5. import android.graphics.Canvas;  
  6. import android.graphics.RectF;  
  7.   
  8. public class Bird  
  9. {  
  10.     /** 
  11.      * 鸟在屏幕高度的2/3位置 
  12.      */  
  13.     private static final float RADIO_POS_HEIGHT = 2 / 3F;  
  14.     /** 
  15.      * 鸟的宽度 30dp 
  16.      */  
  17.     private static final int BIRD_SIZE = 30;  
  18.   
  19.     /** 
  20.      * 鸟的横坐标 
  21.      */  
  22.     private int x;  
  23.     /** 
  24.      * 鸟的纵坐标 
  25.      */  
  26.     private int y;  
  27.     /** 
  28.      * 鸟的宽度 
  29.      */  
  30.     private int mWidth;  
  31.     /** 
  32.      * 鸟的高度 
  33.      */  
  34.     private int mHeight;  
  35.   
  36.     /** 
  37.      * 鸟的bitmap 
  38.      */  
  39.     private Bitmap bitmap;  
  40.     /** 
  41.      * 鸟绘制的范围 
  42.      */  
  43.     private RectF rect new RectF();  
  44.   
  45.     public Bird(Context context, int gameWith, int gameHeight, Bitmap bitmap)  
  46.     {  
  47.   
  48.         this.bitmap = bitmap;  
  49.         //鸟的位置  
  50.         x = gameWith / 2 - bitmap.getWidth() / 2;  
  51.         y = (int) (gameHeight * RADIO_POS_HEIGHT);  
  52.   
  53.         // 计算鸟的宽度和高度  
  54.         mWidth = Util.dp2px(context, BIRD_SIZE);  
  55.         mHeight = (int) (mWidth * 1.0f / bitmap.getWidth() * bitmap.getHeight());  
  56.     }  
  57.   
  58.     /** 
  59.      * 绘制自己 
  60.      *  
  61.      * @param canvas 
  62.      */  
  63.     public void draw(Canvas canvas)  
  64.     {  
  65.         rect.set(x, y, x + mWidth, y + mHeight);  
  66.         canvas.drawBitmap(bitmap, null, rect, null);  
  67.   
  68.     }  
  69.   
  70.     public int getY()  
  71.     {  
  72.         return y;  
  73.     }  
  74.   
  75.     public void setY(int y)  
  76.     {  
  77.         this.y = y;  
  78.     }  
  79.   
  80.     public int getWidth()  
  81.     {  
  82.         return mWidth;  
  83.     }  
  84.   
  85.     public int getHeight()  
  86.     {  
  87.         return mHeight;  
  88.     }  
  89.   
  90. }  

定义了一个类,代表我们的鸟,以及一堆成员变量,并且提供一个draw方法对外;
在GameFlabbyBird中,只需要,初始化我们的Bird,在draw里面调用bird.draw即可;
部分筛检后代码:

  1. public class CopyOfGameFlabbyBird extends SurfaceView implements Callback,  
  2.         Runnable  
  3. {  
  4.     /** 
  5.      * *********鸟相关********************** 
  6.      */  
  7.     private Bird mBird;  
  8.     private Bitmap mBirdBitmap;  
  9.   
  10.     /** 
  11.      * 初始化图片 
  12.      */  
  13.     private void initBitmaps()  
  14.     {  
  15.         mBg = loadImageByResId(R.drawable.bg1);  
  16.         mBirdBitmap = loadImageByResId(R.drawable.b1);  
  17.   
  18.     }  
  19.   
  20.     private void draw()  
  21.     {  
  22.         // drawSomething..  
  23.   
  24.         drawBg();  
  25.         drawBird();  
  26.   
  27.     }  
  28.   
  29.     private void drawBird()  
  30.     {  
  31.         mBird.draw(mCanvas);  
  32.     }  
  33.   
  34.     /** 
  35.      * 初始化尺寸相关 
  36.      */  
  37.     @Override  
  38.     protected void onSizeChanged(int w, int h, int oldw, int oldh)  
  39.     {  
  40.         // 初始化mBird  
  41.         mBird = new Bird(getContext(), mWidth, mHeight, mBirdBitmap);  
  42.   
  43.     }  
  44.   
  45. }  
是不是很简单,下面看下此时效果图
Activity里面这么调用即可:
  1. package com.zhy.surfaceViewDemo;  
  2.   
  3. import com.zhy.view.GameFlabbyBird;  
  4.   
  5. import android.app.Activity;  
  6. import android.os.Bundle;  
  7. import android.view.Window;  
  8. import android.view.WindowManager;  
  9.   
  10. public class MainActivity extends Activity  
  11. {  
  12.     GameFlabbyBird mGame;  
  13.   
  14.     @Override  
  15.     protected void onCreate(Bundle savedInstanceState)  
  16.     {  
  17.         super.onCreate(savedInstanceState);  
  18.         getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,  
  19.                 WindowManager.LayoutParams.FLAG_FULLSCREEN);  
  20.         requestWindowFeature(Window.FEATURE_NO_TITLE);  
  21.         mGame = new GameFlabbyBird(this);  
  22.         setContentView(mGame);  
  23.   
  24.     }  
  25.   
  26. }  


不管咋样,我们的鸟已经在指定的位置了~~~有木有一点小激动~~
下面开始添加地板;

3、绘制地板

绘制地板相比来说会难一点,因为我们需要考虑怎么让地板运动,起初我截取了两个大图,希望通过两张图不断变化,产生动画效果,but,动画的太卡,有跳跃感;
于是,我忽然想到了一个东西可以做,我就把基础图变成了这样:
很小的一块图,先不考虑运动,如何填充成我们目标效果呢?
还记得有个类叫做BitmapShader么?我们可以利用它进行填充。
首先我们依旧是定义一个地板类:Floor
  1. package com.zhy.view;  
  2.   
  3. import java.util.concurrent.TimeUnit;  
  4.   
  5. import com.zhy.surfaceViewDemo.Config;  
  6.   
  7. import android.content.Context;  
  8. import android.graphics.Bitmap;  
  9. import android.graphics.BitmapShader;  
  10. import android.graphics.Canvas;  
  11. import android.graphics.Paint;  
  12. import android.graphics.Paint.Style;  
  13. import android.graphics.Shader.TileMode;  
  14.   
  15. public class Floor  
  16. {  
  17.     /* 
  18.      * 地板位置游戏面板高度的4/5到底部 
  19.      */  
  20.     private static final float FLOOR_Y_POS_RADIO = 4 / 5F; // height of 4/5  
  21.   
  22.     /** 
  23.      * x坐标 
  24.      */  
  25.     private int x;  
  26.     /** 
  27.      * y坐标 
  28.      */  
  29.     private int y;  
  30.     /** 
  31.      * 填充物 
  32.      */  
  33.     private BitmapShader mFloorShader;  
  34.   
  35.     private int mGameWidth;  
  36.   
  37.     private int mGameHeight;  
  38.   
  39.     public Floor(int gameWidth, int gameHeight, Bitmap floorBg)  
  40.     {  
  41.         mGameWidth = gameWidth;  
  42.         mGameHeight = gameHeight;  
  43.         y = (int) (gameHeight * FLOOR_Y_POS_RADIO);  
  44.         mFloorShader = new BitmapShader(floorBg, TileMode.REPEAT,  
  45.                 TileMode.CLAMP);  
  46.     }  
  47.   
  48.     /** 
  49.      * 绘制自己 
  50.      *  
  51.      * @param mCanvas 
  52.      * @param mPaint 
  53.      */  
  54.     public void draw(Canvas mCanvas, Paint mPaint)  
  55.     {  
  56.         if (-x > mGameWidth)  
  57.         {  
  58.             x = x % mGameWidth;  
  59.         }  
  60.         mCanvas.save(Canvas.MATRIX_SAVE_FLAG);  
  61.         //移动到指定的位置  
  62.         mCanvas.translate(x, y);  
  63.         mPaint.setShader(mFloorShader);  
  64.         mCanvas.drawRect(x, 0, -x + mGameWidth, mGameHeight - y, mPaint);  
  65.         mCanvas.restore();  
  66.         mPaint.setShader(null);  
  67.     }  
  68.   
  69.     public int getX()  
  70.     {  
  71.         return x;  
  72.     }  
  73.   
  74.     public void setX(int x)  
  75.     {  
  76.         this.x = x;  
  77.     }  
  78.   
  79. }  
定义了一堆成员变量,核心就在于,我们传入地板背景的填充物,然后初始化我们的mFloorShader,横向重复,纵向拉伸(这里的拉伸是指,纵向的最后一个像素不断重复)。
我们对外公布了draw方法,传入Canvas,我们首先调用canvas.save(),然后将canvas移动到指定的位置,然后绘制我们的矩形,矩形的填充就是我们的地板了~~;
这里,注意一下,我们这里使用了一个变量x,而不是0;为什么呢?因为我们的地板需要利用这个x运动。
那么现在我们如何才能动呢?
首先我们在GameFlabbyBird定义一个变量,表示移动速度mSpeed,然后在draw中不断更新mFloor的x坐标为:mFloor.setX(mFloor.getX() - mSpeed);
这样的画,每次绘制我们floor的起点,会向左移动mSpeed个位置,就形成了运行的效果;但是呢?不能一直减下去,不然最终我们的x岂不是负无穷了,那得绘制多大?
所以我们:
if (-x > mGameWidth)
{
x = x % mGameWidth;
}
如果x的正值大于宽度了,我们取余一下~~~
最终我们的绘制范围是:
mCanvas.drawRect(x, 0, -x + mGameWidth, mGameHeight - y, mPaint);
ok,贴下筛检后GameFlabbyBird代码:

  1. package com.zhy.view;  
  2.   
  3. import android.content.Context;  
  4. import android.graphics.Bitmap;  
  5. import android.graphics.BitmapFactory;  
  6. import android.graphics.Canvas;  
  7. import android.graphics.Paint;  
  8. import android.graphics.PixelFormat;  
  9. import android.graphics.RectF;  
  10. import android.util.AttributeSet;  
  11. import android.util.Log;  
  12. import android.view.SurfaceHolder;  
  13. import android.view.SurfaceHolder.Callback;  
  14. import android.view.SurfaceView;  
  15.   
  16. import com.zhy.surfaceViewDemo.R;  
  17.   
  18. public class CopyOfGameFlabbyBird extends SurfaceView implements Callback,  
  19.         Runnable  
  20. {  
  21.     private Paint mPaint;  
  22.     /** 
  23.      * 地板 
  24.      */  
  25.     private Floor mFloor;  
  26.     private Bitmap mFloorBg;  
  27.   
  28.     private int mSpeed;  
  29.   
  30.     public CopyOfGameFlabbyBird(Context context, AttributeSet attrs)  
  31.     {  
  32.         super(context, attrs);  
  33.   
  34.         mPaint = new Paint();  
  35.         mPaint.setAntiAlias(true);  
  36.         mPaint.setDither(true);  
  37.   
  38.         initBitmaps();  
  39.   
  40.         // 初始化速度  
  41.         mSpeed = Util.dp2px(getContext(), 2);  
  42.   
  43.     }  
  44.   
  45.     /** 
  46.      * 初始化图片 
  47.      */  
  48.     private void initBitmaps()  
  49.     {  
  50.         mFloorBg = loadImageByResId(R.drawable.floor_bg2);  
  51.   
  52.     }  
  53.   
  54.     private void draw()  
  55.     {  
  56.   
  57.         // drawSomething..  
  58.   
  59.         drawBg();  
  60.         drawBird();  
  61.         drawFloor();  
  62.   
  63.         // 更新我们地板绘制的x坐标  
  64.         mFloor.setX(mFloor.getX() - mSpeed);  
  65.   
  66.     }  
  67.   
  68.     private void drawFloor()  
  69.     {  
  70.         mFloor.draw(mCanvas, mPaint);  
  71.     }  
  72.   
  73.     @Override  
  74.     protected void onSizeChanged(int w, int h, int oldw, int oldh)  
  75.     {  
  76.         // 初始化地板  
  77.         mFloor = new Floor(mWidth, mHeight, mFloorBg);  
  78.   
  79.     }  
  80.   
  81. }  

好了,最后剩下个管道了~~~

4、绘制管道

然后是写搞一个管道类Pipe,注意我们的管道分为上下,每个管道的高度可能不同,所以会多一些成员变量;
  1. package com.zhy.view;  
  2.   
  3. import java.util.Random;  
  4.   
  5. import android.content.Context;  
  6. import android.graphics.Bitmap;  
  7. import android.graphics.Canvas;  
  8. import android.graphics.RectF;  
  9.   
  10. /** 
  11.  * 管道分为上下 
  12.  *  
  13.  * @author zhy 
  14.  *  
  15.  */  
  16. public class Pipe  
  17. {  
  18.     /** 
  19.      * 上下管道间的距离 
  20.      */  
  21.     private static final float RADIO_BETWEEN_UP_DOWN = 1 / 5F;  
  22.     /** 
  23.      * 上管道的最大高度 
  24.      */  
  25.     private static final float RADIO_MAX_HEIGHT = 2 / 5F;  
  26.     /** 
  27.      * 上管道的最小高度 
  28.      */  
  29.     private static final float RADIO_MIN_HEIGHT = 1 / 5F;  
  30.     /** 
  31.      * 管道的横坐标 
  32.      */  
  33.     private int x;  
  34.     /** 
  35.      * 上管道的高度 
  36.      */  
  37.     private int height;  
  38.     /** 
  39.      * 上下管道间的距离 
  40.      */  
  41.     private int margin;  
  42.     /** 
  43.      * 上管道图片 
  44.      */  
  45.     private Bitmap mTop;  
  46.     /** 
  47.      * 下管道图片 
  48.      */  
  49.     private Bitmap mBottom;  
  50.   
  51.     private static Random random = new Random();  
  52.   
  53.     public Pipe(Context context, int gameWidth, int gameHeight, Bitmap top,  
  54.             Bitmap bottom)  
  55.     {  
  56.         margin = (int) (gameHeight * RADIO_BETWEEN_UP_DOWN);  
  57.         // 默认从最左边出现  
  58.         x = gameWidth;  
  59.   
  60.         mTop = top;  
  61.         mBottom = bottom;  
  62.   
  63.         randomHeight(gameHeight);  
  64.   
  65.     }  
  66.   
  67.     /** 
  68.      * 随机生成一个高度 
  69.      */  
  70.     private void randomHeight(int gameHeight)  
  71.     {  
  72.         height = random  
  73.                 .nextInt((int) (gameHeight * (RADIO_MAX_HEIGHT - RADIO_MIN_HEIGHT)));  
  74.         height = (int) (height + gameHeight * RADIO_MIN_HEIGHT);  
  75.     }  
  76.   
  77.     public void draw(Canvas mCanvas, RectF rect)  
  78.     {  
  79.         mCanvas.save(Canvas.MATRIX_SAVE_FLAG);  
  80.         // rect为整个管道,假设完整管道为100,需要绘制20,则向上偏移80  
  81.         mCanvas.translate(x, -(rect.bottom - height));  
  82.         mCanvas.drawBitmap(mTop, null, rect, null);  
  83.         // 下管道,便宜量为,上管道高度+margin  
  84.         mCanvas.translate(0, (rect.bottom - height) + height + margin);  
  85.         mCanvas.drawBitmap(mBottom, null, rect, null);  
  86.         mCanvas.restore();  
  87.     }  
  88.   
  89.     public int getX()  
  90.     {  
  91.         return x;  
  92.     }  
  93.   
  94.     public void setX(int x)  
  95.     {  
  96.         this.x = x;  
  97.     }  
  98.   
  99. }  

我们直接看draw方法,我们的传入的rect是固定的一个矩形,我们的上下管道都是完整的绘制在这个rect中;
然后根据height,去偏移canvas的y,让rect显示出height部分,主要是因为,这样可以保证每个管道样子是一样的(如果根据height,使用不同的rect,会产生缩放);
Pipe写好了~~我们需要在GameFlabbyBird中去使用;但是考虑一下,游戏中的管道不像鸟和地面,有很多个,且是在运行中不断生成新的~~~
所以我们保存Pipe最起码是个List<Pipe>
筛检后的代码:
  1. public class GameFlabbyBird extends SurfaceView implements Callback, Runnable  
  2. {  
  3.   
  4.     /** 
  5.      * *********管道相关********************** 
  6.      */  
  7.     /** 
  8.      * 管道 
  9.      */  
  10.     private Bitmap mPipeTop;  
  11.     private Bitmap mPipeBottom;  
  12.     private RectF mPipeRect;  
  13.     private int mPipeWidth;  
  14.     /** 
  15.      * 管道的宽度 60dp 
  16.      */  
  17.     private static final int PIPE_WIDTH = 60;  
  18.   
  19.     private List<Pipe> mPipes = new ArrayList<Pipe>();  
  20.   
  21.     public GameFlabbyBird(Context context, AttributeSet attrs)  
  22.     {  
  23.         super(context, attrs);  
  24.         mPipeWidth = Util.dp2px(getContext(), PIPE_WIDTH);  
  25.   
  26.     }  
  27.   
  28.     /** 
  29.      * 初始化图片 
  30.      */  
  31.     private void initBitmaps()  
  32.     {  
  33.         ;  
  34.         mPipeTop = loadImageByResId(R.drawable.g2);  
  35.         mPipeBottom = loadImageByResId(R.drawable.g1);  
  36.     }  
  37.   
  38.     private void draw()  
  39.     {  
  40.   
  41.         drawBg();  
  42.         drawBird();  
  43.         drawPipes();  
  44.         drawFloor();  
  45.   
  46.     }  
  47.   
  48.     /** 
  49.      * 绘制管道 
  50.      */  
  51.     private void drawPipes()  
  52.     {  
  53.         for (Pipe pipe : mPipes)  
  54.         {  
  55.             pipe.setX(pipe.getX() - mSpeed);  
  56.             pipe.draw(mCanvas, mPipeRect);  
  57.         }  
  58.     }  
  59.   
  60.     /** 
  61.      * 初始化尺寸相关 
  62.      */  
  63.     @Override  
  64.     protected void onSizeChanged(int w, int h, int oldw, int oldh)  
  65.     {  
  66.         super.onSizeChanged(w, h, oldw, oldh);  
  67.   
  68.         // 初始化管道范围  
  69.         mPipeRect = new RectF(00, mPipeWidth, mHeight);  
  70.         Pipe pipe = new Pipe(getContext(), w, h, mPipeTop, mPipeBottom);  
  71.         mPipes.add(pipe);  
  72.   
  73.     }  
  74.   
  75. }  

我们在onSizeChanged中初始化了一个Pipe,添加到了mPipes中,然后在draw里面,动态改变Pipe的x为pipe.setX(pipe.getX() - mSpeed);
下面来看下效果:
我们的管道从右侧进入界面,然后消失在左侧~
当然了,关于管道还有很多需要编写,比如管道每隔多远生成一个,也不能让无限生成,当管道从界面移除应该从mPipes中移出;
以及判断管道和鸟的碰撞,这些都放置到下一篇博客叙述~~

5、绘制分数

分数的绘制比较简单,我准备了10个图,对应于0-9
没有单独定义类了,直接写了~~
筛检后的代码:

沒有留言:

張貼留言