好久没有更新博客了,最近试着做了一下之前火过一段时间的小游戏Bird fly,来跟大家分享一下我的经验。
首先上一个效果图:
我们来简单做一下第一步分析:
1、绘制游戏背景。
2、绘制水管。
3、绘制地板的背景。
4、绘制那只小鸟。
5、绘制分数
接下来第二步的分析:
1、底板的移动
2、水管默认的向左移动
3、小鸟的自动下落,以及碰到水管或地板之后游戏over
4、水管的自动生成以及移除
接下来可以敲代码了。
一、SurfaceView的大众写法:
package com.jp.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 BaseGameBirdextends 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);
}
}
}
一般 SurfaceView的基础都是这么写的,大家可以拷过去直接用;
二、背景图的绘制:
背景其实也就是一张图片。
/**
* 当前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);
}
通过mCanvas.drawBitmap(),设置背景。这个也是BaseGameBird类里的内容
三、绘制小鸟
小鸟处于屏幕正中央,所以x为屏幕的一般,高度么,这个其实可以看个人爱好,我这边设置的是1/3f。
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.RectF;
/**
* Created by Administrator on 2016/1/5.
*/
public class Bird
{
/**
* 鸟在屏幕的2/3的位置
*/
private static final float BIRD_HIGHT = 1 / 3f;
/**
* 鸟的宽度
*/
private static final int BIRD_WIDTH_SIZE = 30;
/**
* 鸟的高宽度
*/
private int mBirdHeight, mBirdWidth;
/**
* 鸟的横竖坐标
*/
private int x, y;
/**
* 鸟的绘制范围
*/
private RectF mBirdRecf = new RectF();
/**
* 鸟的bitmap
*/
private Bitmap bitmap;
public Bird(Context context,int height, int width, Bitmap bitmap)
{
this.bitmap=bitmap;
//鸟的位置
System.out.println(width);
System.out.println(bitmap.getWidth());
x=width/2-bitmap.getWidth()/2;
y= (int) (height*BIRD_HIGHT);
mBirdWidth=Util.dp2px(context,BIRD_WIDTH_SIZE);
mBirdHeight= (int) (mBirdWidth*1.0f/bitmap.getWidth()*bitmap.getHeight());
}
/**
* 绘制自己
* @param canvas
*/
public void draw(Canvas canvas){
mBirdRecf.set(x,y,x+mBirdWidth,y+mBirdHeight);
canvas.drawBitmap(bitmap,null,mBirdRecf,null);
}
public int getY()
{
return y;
}
public void setY(int y)
{
this.y = y;
}
public int getX()
{
return x;
}
public void setX(int x)
{
this.x = x;
}
public int getWidth()
{
return mBirdWidth;
}
public int getHeight()
{
return mBirdHeight;
}
}
接下来只需要在我们BaseGameBird,初始化一下,调用draw()传入需要的属性即可;
筛选后代码:
/**
* *********鸟相关**********************
*/
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);
}
在MainActivity中先调用一下,看一下效果图:
package com.jp.birddemo.activity;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.view.Window;
import android.view.WindowManager;
import tool.BaseGameBird;
public class MainActivity extends FragmentActivity
{
private BaseGameBird baseGameBird;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
requestWindowFeature(Window.FEATURE_NO_TITLE);
baseGameBird =new BaseGameBird(this);
setContentView(baseGameBird);
}
}
三、底板绘制
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Shader;
/**
* Created by Administrator on 2016/1/7.
*/
public class Floor
{
/**
* 地板的位置
*/
private static final float FLOOR_Y = 3 / 4F;
/**
* 地板的坐标
*/
private int x, y;
/**
* 地板的图形
*/
private BitmapShader mBitmapSharder;
private int mFloorHeight, mFloorWidth;
public Floor(int floorHeight, int floorWidth, Bitmap bitmap)
{
this.mFloorHeight = floorHeight;
this.mFloorWidth = floorWidth;
y = (int) (floorHeight * FLOOR_Y);
mBitmapSharder = new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.CLAMP);
}
public void draw(Canvas mCanvas, Paint mPaint)
{
if (-x > mFloorWidth) {
x = x % mFloorWidth;
}
mCanvas.save(Canvas.MATRIX_SAVE_FLAG);
mCanvas.translate(x, y);
mPaint.setShader(mBitmapSharder);
mCanvas.drawRect(x, 0, -x + mFloorWidth, mFloorHeight - y, mPaint);
mCanvas.restore();
mPaint.setShader(null);
}
public void setX(int x)
{
this.x=x;
}
public int getX()
{
return x;
}
public int getY()
{
return y;
}
}
定义了一堆成员变量,核心就在于,我们传入地板背景的填充物,然后初始化我们的mFloorShader,横向重复,纵向拉伸(这里的拉伸是指,纵向的最后一个像素不断重复)。
我们对外公布了draw方法,传入Canvas,我们首先调用canvas.save(),然后将canvas移动到指定的位置,然后绘制我们的矩形,矩形的填充就是我们的地板了; ~
这里,注意一下,我们这里使用了一个变量x,而不是0;为什么呢?因为我们的地板需要利用这个x运动。
那么现在我们如何才能动呢?
首先我们在BaseGameBird定义一个变量,表示移动速度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,贴下筛检后BaseGameBird代码:
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);
}
通过mFloor.setX(mFloor.getX() - mSpeed)的设置,表现出地板在移动的效果。
四、水管的绘制
水管的特点:
1、上下水管的距离都是固定的,这里我们设置为1 / 5F
2、上下水管出现的时候长短不一
好了,简单先这样
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.RectF;
import java.util.Random;
/**
* Created by Administrator on 2016/1/7.
*/
public class Pipe
{
/**
* 上下管道的距离
*/
private static final float PIPE_LONG_SIZE = 1 / 5F;
/**
* 上管道的最大高度
*/
private static final float PIPE_TOP_SIZE = 2 / 5F;
/**
* 下管道的最大高度
*/
private static final float PIPE_BOTTOM_SIZE = 1 / 5f;
/**
* 管道的横坐标
*/
private int x;
/**
* 上管道的高度
*/
private int mHeight;
/**
* 两个管道的距离
*/
private int margin;
/**
* 上下管道图片
*/
private Bitmap mTopBitmap, mBottomBitmap;
/**
* 用于产生随机高度
*/
private static Random random = new Random();
public Pipe(Context context, int mPipeHeight, int mPipeWeight, Bitmap mTopBitmap, Bitmap mBottomBitmap)
{
margin = (int) (mPipeHeight * PIPE_LONG_SIZE);
x = mPipeWeight;
this.mTopBitmap = mTopBitmap;
this.mBottomBitmap = mBottomBitmap;
getRandomHeight(mPipeHeight);
}
private void getRandomHeight(int mPipeHeight)
{
mHeight = random.nextInt((int) (mPipeHeight * (PIPE_TOP_SIZE - PIPE_BOTTOM_SIZE)));
mHeight = (int) (mHeight + mPipeHeight * PIPE_BOTTOM_SIZE);
}
public void draw(Canvas canvas, RectF rectF)
{
canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.translate(x, -(rectF.bottom - mHeight));
canvas.drawBitmap(mTopBitmap, null, rectF, null);
canvas.translate(0, (rectF.bottom - mHeight) + mHeight + margin);
canvas.drawBitmap(mBottomBitmap, null, rectF, null);
canvas.restore();
}
public int getX()
{
return x;
}
public void setX(int x)
{
this.x = x;
}
}
BaseGameBird筛检后的代码:
/**
* *********管道相关**********************
*/
/**
* 管道
*/
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);
}
五、分数绘制:
/**
* 分数
*/
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);
}
这里还要说一下,我上面用到转换px的方法:
public static int dp2px(Context context, float dp)
{
int px = Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics()));
return px;
}
好了先这样,明天更新全部教程,以及提供源代码