哈,记得以前写过Android SurfaceView实战 打造抽奖转盘 , 同属于SurfaceView系列,基本可以从这篇博文中学习到SurfaceView的用法,以及利用SurfaceView做抽奖转盘。但是其中缺少一部分的知识点,就是与用户交互时界面的改变,所以今天给大家再带来本篇博文教大家如何做flabby bird这款游戏,这游戏虽然不难,但是也为其作者挣了不少钱,大家在学会以后,可以尽可能发挥自己的创意,做属于自己的游戏,说不定下一个火的奏是你。
ok,那么首先上下效果图:
再来张动态的:
由于上传图片最大限制为2M,所以做了压缩处理,凑合看吧 ~~~
仔细观察游戏,需要绘制的有:背景、地板、鸟、管道、分数;
游戏开始时:
地板给人一种想左移动的感觉;
管道与地板同样的速度向左移动;
鸟默认下落;
当用户touch屏幕时,鸟上升一段距离后,下落;
运动过程中需要判断管道和鸟之间的位置关系,是否触碰,是否穿过等,需要计算分数。
好了,大概就这么多,那我们首先开始考虑绘制~~~
接下来,我们首先编写下SurfaceView的一般写法:
package com.zhy.view; import android.content.Context; import android.graphics.Canvas; import android.graphics.PixelFormat; import android.util.AttributeSet; import android.view.SurfaceHolder; import android.view.SurfaceHolder.Callback; import android.view.SurfaceView; public class GameFlabbyBird extends SurfaceView implements Callback, Runnable { private SurfaceHolder mHolder; /** * 与SurfaceHolder绑定的Canvas */ private Canvas mCanvas; /** * 用于绘制的线程 */ private Thread t; /** * 线程的控制开关 */ private boolean isRunning; public GameFlabbyBird(Context context) { this(context, null); } public GameFlabbyBird(Context context, AttributeSet attrs) { super(context, attrs); mHolder = getHolder(); mHolder.addCallback(this); setZOrderOnTop(true);// 设置画布 背景透明 mHolder.setFormat(PixelFormat.TRANSLUCENT); // 设置可获得焦点 setFocusable(true); setFocusableInTouchMode(true); // 设置常亮 this.setKeepScreenOn(true); } @Override public void surfaceCreated(SurfaceHolder holder) { // 开启线程 isRunning = true; t = new Thread(this); t.start(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // TODO Auto-generated method stub } @Override public void surfaceDestroyed(SurfaceHolder holder) { // 通知关闭线程 isRunning = false; } @Override public void run() { while (isRunning) { long start = System.currentTimeMillis(); draw(); long end = System.currentTimeMillis(); try { if (end - start < 50) { Thread.sleep(50 - (end - start)); } } catch (InterruptedException e) { e.printStackTrace(); } } } private void draw() { try { // 获得canvas mCanvas = mHolder.lockCanvas(); if (mCanvas != null) { // drawSomething.. } } catch (Exception e) { } finally { if (mCanvas != null) mHolder.unlockCanvasAndPost(mCanvas); } } }
最简单的当然是背景了,直接drawBitmap即可。
我们添加需要的成员变量,以及初始化一些参数,然后添加drawBg方法,最后在draw中调用drawBg;
public class CopyOfGameFlabbyBird extends SurfaceView implements Callback, Runnable { /** * 当前View的尺寸 */ private int mWidth; private int mHeight; private RectF mGamePanelRect = new RectF(); /** * 背景 */ private Bitmap mBg; public CopyOfGameFlabbyBird(Context context, AttributeSet attrs) { //省略了很多代码 initBitmaps(); } /** * 初始化图片 */ private void initBitmaps() { mBg = loadImageByResId(R.drawable.bg1); } private void draw() { //省略了很多代码 drawBg(); //省略了很多代码 } /** * 绘制背景 */ private void drawBg() { mCanvas.drawBitmap(mBg, null, mGamePanelRect, null); } /** * 初始化尺寸相关 */ @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mWidth = w; mHeight = h; mGamePanelRect.set(0, 0, w, h); } /** * 根据resId加载图片 * * @param resId * @return */ private Bitmap loadImageByResId(int resId) { return BitmapFactory.decodeResource(getResources(), resId); } }
好了,现在背景图绘制好了,接下来,我们绘制小鸟~~~
鸟在我们的屏幕中,初始化时需要一个位置,x上,肯定是居中,y上我们取2/3的高度;
关于bird,我们单独创建一个类:
package com.zhy.view; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.RectF; public class Bird { /** * 鸟在屏幕高度的2/3位置 */ private static final float RADIO_POS_HEIGHT = 2 / 3F; /** * 鸟的宽度 30dp */ private static final int BIRD_SIZE = 30; /** * 鸟的横坐标 */ private int x; /** * 鸟的纵坐标 */ private int y; /** * 鸟的宽度 */ private int mWidth; /** * 鸟的高度 */ private int mHeight; /** * 鸟的bitmap */ private Bitmap bitmap; /** * 鸟绘制的范围 */ private RectF rect = new RectF(); public Bird(Context context, int gameWith, int gameHeight, Bitmap bitmap) { this.bitmap = bitmap; //鸟的位置 x = gameWith / 2 - bitmap.getWidth() / 2; y = (int) (gameHeight * RADIO_POS_HEIGHT); // 计算鸟的宽度和高度 mWidth = Util.dp2px(context, BIRD_SIZE); mHeight = (int) (mWidth * 1.0f / bitmap.getWidth() * bitmap.getHeight()); } /** * 绘制自己 * * @param canvas */ public void draw(Canvas canvas) { rect.set(x, y, x + mWidth, y + mHeight); canvas.drawBitmap(bitmap, null, rect, null); } public int getY() { return y; } public void setY(int y) { this.y = y; } public int getWidth() { return mWidth; } public int getHeight() { return mHeight; } }
在GameFlabbyBird中,只需要,初始化我们的Bird,在draw里面调用bird.draw即可;
部分筛检后代码:
public class CopyOfGameFlabbyBird extends SurfaceView implements Callback, Runnable { /** * *********鸟相关********************** */ private Bird mBird; private Bitmap mBirdBitmap; /** * 初始化图片 */ private void initBitmaps() { mBg = loadImageByResId(R.drawable.bg1); mBirdBitmap = loadImageByResId(R.drawable.b1); } private void draw() { // drawSomething.. drawBg(); drawBird(); } private void drawBird() { mBird.draw(mCanvas); } /** * 初始化尺寸相关 */ @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { // 初始化mBird mBird = new Bird(getContext(), mWidth, mHeight, mBirdBitmap); } }是不是很简单,下面看下此时效果图:
Activity里面这么调用即可:
package com.zhy.surfaceViewDemo; import com.zhy.view.GameFlabbyBird; import android.app.Activity; import android.os.Bundle; import android.view.Window; import android.view.WindowManager; public class MainActivity extends Activity { GameFlabbyBird mGame; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); requestWindowFeature(Window.FEATURE_NO_TITLE); mGame = new GameFlabbyBird(this); setContentView(mGame); } }
不管咋样,我们的鸟已经在指定的位置了~~~有木有一点小激动~~
下面开始添加地板;
绘制地板相比来说会难一点,因为我们需要考虑怎么让地板运动,起初我截取了两个大图,希望通过两张图不断变化,产生动画效果,but,动画的太卡,有跳跃感;
于是,我忽然想到了一个东西可以做,我就把基础图变成了这样:
很小的一块图,先不考虑运动,如何填充成我们目标效果呢?
还记得有个类叫做BitmapShader么?我们可以利用它进行填充。
相关知识可以参考:Android BitmapShader 实战 实现圆形、圆角图片
首先我们依旧是定义一个地板类:Floor
package com.zhy.view; import java.util.concurrent.TimeUnit; import com.zhy.surfaceViewDemo.Config; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.Shader.TileMode; public class Floor { /* * 地板位置游戏面板高度的4/5到底部 */ private static final float FLOOR_Y_POS_RADIO = 4 / 5F; // height of 4/5 /** * x坐标 */ private int x; /** * y坐标 */ private int y; /** * 填充物 */ private BitmapShader mFloorShader; private int mGameWidth; private int mGameHeight; public Floor(int gameWidth, int gameHeight, Bitmap floorBg) { mGameWidth = gameWidth; mGameHeight = gameHeight; y = (int) (gameHeight * FLOOR_Y_POS_RADIO); mFloorShader = new BitmapShader(floorBg, TileMode.REPEAT, TileMode.CLAMP); } /** * 绘制自己 * * @param mCanvas * @param mPaint */ public void draw(Canvas mCanvas, Paint mPaint) { if (-x > mGameWidth) { x = x % mGameWidth; } mCanvas.save(Canvas.MATRIX_SAVE_FLAG); //移动到指定的位置 mCanvas.translate(x, y); mPaint.setShader(mFloorShader); mCanvas.drawRect(x, 0, -x + mGameWidth, mGameHeight - y, mPaint); mCanvas.restore(); mPaint.setShader(null); } public int getX() { return x; } public void setX(int x) { this.x = x; } }
我们对外公布了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代码:
package com.zhy.view; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.RectF; import android.util.AttributeSet; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceHolder.Callback; import android.view.SurfaceView; import com.zhy.surfaceViewDemo.R; public class CopyOfGameFlabbyBird extends SurfaceView implements Callback, Runnable { private Paint mPaint; /** * 地板 */ private Floor mFloor; private Bitmap mFloorBg; private int mSpeed; public CopyOfGameFlabbyBird(Context context, AttributeSet attrs) { super(context, attrs); mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setDither(true); initBitmaps(); // 初始化速度 mSpeed = Util.dp2px(getContext(), 2); } /** * 初始化图片 */ private void initBitmaps() { mFloorBg = loadImageByResId(R.drawable.floor_bg2); } private void draw() { // drawSomething.. drawBg(); drawBird(); drawFloor(); // 更新我们地板绘制的x坐标 mFloor.setX(mFloor.getX() - mSpeed); } private void drawFloor() { mFloor.draw(mCanvas, mPaint); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { // 初始化地板 mFloor = new Floor(mWidth, mHeight, mFloorBg); } }
现在的效果:
好了,最后剩下个管道了~~~
然后是写搞一个管道类Pipe,注意我们的管道分为上下,每个管道的高度可能不同,所以会多一些成员变量;
package com.zhy.view; import java.util.Random; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.RectF; /** * 管道分为上下 * * @author zhy * */ public class Pipe { /** * 上下管道间的距离 */ private static final float RADIO_BETWEEN_UP_DOWN = 1 / 5F; /** * 上管道的最大高度 */ private static final float RADIO_MAX_HEIGHT = 2 / 5F; /** * 上管道的最小高度 */ private static final float RADIO_MIN_HEIGHT = 1 / 5F; /** * 管道的横坐标 */ private int x; /** * 上管道的高度 */ private int height; /** * 上下管道间的距离 */ private int margin; /** * 上管道图片 */ private Bitmap mTop; /** * 下管道图片 */ private Bitmap mBottom; private static Random random = new Random(); public Pipe(Context context, int gameWidth, int gameHeight, Bitmap top, Bitmap bottom) { margin = (int) (gameHeight * RADIO_BETWEEN_UP_DOWN); // 默认从最左边出现 x = gameWidth; mTop = top; mBottom = bottom; randomHeight(gameHeight); } /** * 随机生成一个高度 */ private void randomHeight(int gameHeight) { height = random .nextInt((int) (gameHeight * (RADIO_MAX_HEIGHT - RADIO_MIN_HEIGHT))); height = (int) (height + gameHeight * RADIO_MIN_HEIGHT); } public void draw(Canvas mCanvas, RectF rect) { mCanvas.save(Canvas.MATRIX_SAVE_FLAG); // rect为整个管道,假设完整管道为100,需要绘制20,则向上偏移80 mCanvas.translate(x, -(rect.bottom - height)); mCanvas.drawBitmap(mTop, null, rect, null); // 下管道,便宜量为,上管道高度+margin mCanvas.translate(0, (rect.bottom - height) + height + margin); mCanvas.drawBitmap(mBottom, null, rect, null); mCanvas.restore(); } public int getX() { return x; } public void setX(int x) { this.x = x; } }
然后根据height,去偏移canvas的y,让rect显示出height部分,主要是因为,这样可以保证每个管道样子是一样的(如果根据height,使用不同的rect,会产生缩放);
Pipe写好了~~我们需要在GameFlabbyBird中去使用;但是考虑一下,游戏中的管道不像鸟和地面,有很多个,且是在运行中不断生成新的~~~
所以我们保存Pipe最起码是个List<Pipe>
筛检后的代码:
public class GameFlabbyBird extends SurfaceView implements Callback, Runnable { /** * *********管道相关********************** */ /** * 管道 */ private Bitmap mPipeTop; private Bitmap mPipeBottom; private RectF mPipeRect; private int mPipeWidth; /** * 管道的宽度 60dp */ private static final int PIPE_WIDTH = 60; private List<Pipe> mPipes = new ArrayList<Pipe>(); public GameFlabbyBird(Context context, AttributeSet attrs) { super(context, attrs); mPipeWidth = Util.dp2px(getContext(), PIPE_WIDTH); } /** * 初始化图片 */ private void initBitmaps() { ; mPipeTop = loadImageByResId(R.drawable.g2); mPipeBottom = loadImageByResId(R.drawable.g1); } private void draw() { drawBg(); drawBird(); drawPipes(); drawFloor(); } /** * 绘制管道 */ private void drawPipes() { for (Pipe pipe : mPipes) { pipe.setX(pipe.getX() - mSpeed); pipe.draw(mCanvas, mPipeRect); } } /** * 初始化尺寸相关 */ @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // 初始化管道范围 mPipeRect = new RectF(0, 0, mPipeWidth, mHeight); Pipe pipe = new Pipe(getContext(), w, h, mPipeTop, mPipeBottom); mPipes.add(pipe); } }
下面来看下效果:
我们的管道从右侧进入界面,然后消失在左侧~
当然了,关于管道还有很多需要编写,比如管道每隔多远生成一个,也不能让无限生成,当管道从界面移除应该从mPipes中移出;
以及判断管道和鸟的碰撞,这些都放置到下一篇博客叙述~~
分数的绘制比较简单,我准备了10个图,对应于0-9
没有单独定义类了,直接写了~~
筛检后的代码:
public class GameFlabbyBird extends SurfaceView implements Callback, Runnable { /** * 分数 */ private final int[] mNums = new int[] { R.drawable.n0, R.drawable.n1, R.drawable.n2, R.drawable.n3, R.drawable.n4, R.drawable.n5, R.drawable.n6, R.drawable.n7, R.drawable.n8, R.drawable.n9 }; private Bitmap[] mNumBitmap; private int mGrade = 100; /** * 单个数字的高度的1/15 */ private static final float RADIO_SINGLE_NUM_HEIGHT = 1 / 15f; /** * 单个数字的宽度 */ private int mSingleGradeWidth; /** * 单个数字的高度 */ private int mSingleGradeHeight; /** * 单个数字的范围 */ private RectF mSingleNumRectF; /** * 初始化图片 */ private void initBitmaps() { mNumBitmap = new Bitmap[mNums.length]; for (int i = 0; i < mNumBitmap.length; i++) { mNumBitmap[i] = loadImageByResId(mNums[i]); } } private void draw() { // drawSomething.. drawBg(); drawBird(); drawPipes(); drawFloor(); drawGrades(); } /** * 绘制分数 */ private void drawGrades() { String grade = mGrade + ""; mCanvas.save(Canvas.MATRIX_SAVE_FLAG); mCanvas.translate(mWidth / 2 - grade.length() * mSingleGradeWidth / 2, 1f / 8 * mHeight); // draw single num one by one for (int i = 0; i < grade.length(); i++) { String numStr = grade.substring(i, i + 1); int num = Integer.valueOf(numStr); mCanvas.drawBitmap(mNumBitmap[num], null, mSingleNumRectF, null); mCanvas.translate(mSingleGradeWidth, 0); } mCanvas.restore(); } /** * 初始化尺寸相关 */ @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // 初始化分数 mSingleGradeHeight = (int) (h * RADIO_SINGLE_NUM_HEIGHT); mSingleGradeWidth = (int) (mSingleGradeHeight * 1.0f / mNumBitmap[0].getHeight() * mNumBitmap[0].getWidth()); mSingleNumRectF = new RectF(0, 0, mSingleGradeWidth, mSingleGradeHeight); } }
绘制前,根据数字的位数,对画布进行偏移到中心位置,然后绘制;绘制过程中,每绘制完成一个数字则偏移一个数字的宽度;
现在的效果:
ok,到此为止,我们完成了所有需要绘制的东西~~由于篇幅原因,下一篇,将在此基础上完善剩下的所有内容~~~
有兴趣的,可以在此基础上直接尝试写了~~~
源码点击下载
我建了一个QQ群,方便大家交流。群号:423372824
----------------------------------------------------------------------------------------------------------
博主部分视频已经上线,如果你不喜欢枯燥的文本,请猛戳(初录,期待您的支持):
视频目录地址:本人录制的视频教程