您的位置:首页 > 移动开发 > Android开发

Android游戏开发之构建游戏框架View与SurFaceView的区别

2013-06-09 19:34 591 查看
转自:http://blog.csdn.net/xys289187120/article/details/6632125

[b]雨松MOMO原创文章如转载,请注明:转载至我的独立域名博客雨松MOMO程序研究院,原文地址:http://www.xuanyusong.com/archives/266

[/b]



1.view

view在api中的结构



java.lang.Object
android.view.View

直接子类:
AnalogClock, ImageView, KeyboardView, ProgressBar, SurfaceView, TextVie, ViewGroup, ViewStub


间接子类:
AbsListView, AbsSeekBar, AbsSpinner, AbsoluteLayout, AdapterView<T extends Adapter>, AppWidgetHostView, AutoCompleteTextView, Button, CheckBox, CheckedTextView, Chronometer, CompoundButton,
DatePicker, DialerFilter, DigitalClock,EditView, ExpandableListView, ExtractEditText, FrameLayout, GLSurfaceView, Gallery, GestureOverlayView, GridView, HorizontalScrollView, ImageButton, ImageSwitcher, LinearLayout, ListView, MediaController, MultiAutoCompleteTextView,
QuickContactBadge, RadioButton, RadioGroup, RatingBar, RelativeLayout, ScrollView, SeekBar, SlidingDrawer, Spinner, TabHost, TabWidget, TableLayout, TableRow, TextSwitcher, TimePicker, ToggleButton, TwoLineListItem, VideoView, ViewAnimator, ViewFlipper, ViewSwitcher,
WebView, ZoomButton, ZoomControls


由此可见View类属于Android开发绘制中的显示老大,任何与绘制有关系的控件都是它的子类。在这篇文章中我主要讲View 与SurFaceView 使用线程刷新屏幕绘制方面的知识。开发中如何去选择使用View还是SurFaceView。我相信读过我前几篇博客的朋友应该知道我在刷新屏幕的时候使用invalidate()方法来重绘,下面我详细的说明一下Andooid刷新屏幕的几种方法。

第一种: 在onDraw方法最后调用invalidate()方法,它会通知UI线程重绘 这样 View会重新调用onDraw方法,实现刷新屏幕。 这样写看起来代码非常简洁漂亮,但是它也同时存在一个很大的问题,它和游戏主线程是分开的 它违背了单线程模式,这样操作绘制的话是很不安全的,举个例子 比如程序先进在Activity1中 使用invalidate()方法来重绘,
然后我跳到了Activity2这时候Activity1已经finash()掉 可是Activity1中 的invalidate() 的线程还在程序中,Android的虚拟机不可能主动杀死正在运行中的线程所以这样操作是非常危险的。因为它是在UI线程中被动掉用的所以很不安全。


invalidate() 更新整个屏幕区域



invalidate(Rect rect) 更新Rect区域



invalidate(l, t, r, b) 更新指定矩形区域

[java]
view plaincopyprint?

public void onDraw(Canvas canvas){

DosomeThing();
invalidate();
}

public void onDraw(Canvas canvas){
DosomeThing();
invalidate();
}


第二种:使用postInvalidate();方法来刷新屏幕 ,调用后它会用handler通知UI线程重绘屏幕,我们可以 new Thread(this).start(); 开启一个游戏的主线程 然后在主线程中通过调用postInvalidate();方法来刷新屏幕。postInvalidate();方法 调用后 系统会帮我们调用onDraw方法 ,它是在我们自己的线程中调用 通过调用它可以通知UI线程刷新屏幕
。由此可见它是主动调用UI线程的。所以建议使用postInvalidate()方法通知UI线程来刷新整个屏幕。



postInvalidate(left, top, right, bottom) 方法 通过UI线程来刷新规定矩形区域。

[java]
view plaincopyprint?

@Override
public void run() {

while (mIsRunning) {

try {
Thread.sleep(100);

postInvalidate();
} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();
}
}
}

@Override
public void run() {
while (mIsRunning) {
try {
Thread.sleep(100);
postInvalidate();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}


View中用到的双缓冲技术

重绘的原理是 程序根据时间来刷新屏幕 如果有一帧图形还没有完全绘制结束 程序就开始刷新屏幕这样就会造成瞬间屏幕闪烁 画面很不美观,所以双缓冲的技术就诞生了。它存在的目的就是解决屏幕闪烁的问题,下面我说说在自定义View中如何实现双缓冲。

首先我们需要创建一张屏幕大小的缓冲图片,我说一下第三个参数 ARGB 分别代表的是 透明度 红色 绿色 蓝色

Bitmap.Config ARGB_4444 ARGB 分别占四位

Bitmap.Config ARGB_8888 ARGB 分别占八位

Bitmap.Config RGB_565 没有透明度(A) R占5位 G 占6位 B占5位


一般情况下我们使用ARGB_8888 因为它的效果是最好了 当然它也是最占内存的。

[java]
view plaincopyprint?

mBufferBitmap = Bitmap.createBitmap(mScreenWidth,mScreenHeight,Config.ARGB_8888);

mBufferBitmap = Bitmap.createBitmap(mScreenWidth,mScreenHeight,Config.ARGB_8888);


创建一个缓冲的画布,将内容绘制在缓冲区mBufferBitmap中

[java]
view plaincopyprint?

Canvas mCanvas = new Canvas();

mCanvas.setBitmap(mBufferBitmap);

Canvas mCanvas = new Canvas();
mCanvas.setBitmap(mBufferBitmap);


最后一次性的把缓冲区mBufferBitmap绘制在屏幕上,怎么样 简单吧 呵呵。

[java]
view plaincopyprint?

@Override
protected void onDraw(Canvas canvas) {

/**这里先把所有须要绘制的资源绘制到mBufferBitmap上**/

/**绘制地图**/
DrawMap(mCanvas,mPaint,mBitmap);
/**绘制动画**/
RenderAnimation(mCanvas);
/**更新动画**/
UpdateAnimation();

if(isBorderCollision) {

DrawCollision(mCanvas,"与边界发生碰撞");

}

if(isAcotrCollision) {
DrawCollision(mCanvas,"与实体层发生碰撞");

}
if(isPersonCollision) {

DrawCollision(mCanvas,"与NPC发生碰撞");

}

/**最后通过canvas一次性的把mBufferBitmap绘制到屏幕上**/

canvas.drawBitmap(mBufferBitmap, 0,0, mPaint);

super.onDraw(canvas);

}

@Override
protected void onDraw(Canvas canvas) {
/**这里先把所有须要绘制的资源绘制到mBufferBitmap上**/
/**绘制地图**/
DrawMap(mCanvas,mPaint,mBitmap);
/**绘制动画**/
RenderAnimation(mCanvas);
/**更新动画**/
UpdateAnimation();

if(isBorderCollision) {
DrawCollision(mCanvas,"与边界发生碰撞");
}

if(isAcotrCollision) {
DrawCollision(mCanvas,"与实体层发生碰撞");
}
if(isPersonCollision) {
DrawCollision(mCanvas,"与NPC发生碰撞");
}

/**最后通过canvas一次性的把mBufferBitmap绘制到屏幕上**/
canvas.drawBitmap(mBufferBitmap, 0,0, mPaint);
super.onDraw(canvas);
}


由此可见view属于被动刷新, 因为我们做的任何刷新的操作实际上都是通知UI线程去刷新。所以在做一些只有通过玩家操作以后才会刷新屏幕的游戏 并非自动刷新的游戏 可以使用view来操作。

2.SurfaceView

从API中可以看出SurfaceView属于View的子类 它是专门为制作游戏而产生的,它的功能非常强大,最重要的是它支持OpenGL ES库,2D和3D的效果都可以实现。创建SurfaceView的时候需要实现SurfaceHolder.Callback接口,它可以用来监听SurfaceView的状态,SurfaceView的改变 SurfaceView的创建 SurfaceView
销毁 我们可以在相应的方法中做一些比如初始化的操作 或者 清空的操作等等。


使用SurfaceView构建游戏框架它的绘制原理是绘制前先锁定画布 然后等都绘制结束以后 在对画布进行解锁 最后在把画布内容显示到屏幕上。

代码中是如何实现SurfaceView

首先需要实现 Callback 接口 与Runnable接口


[java]
view plaincopyprint?

public class AnimView
extends SurfaceView implements Callback,Runnable

public class AnimView extends SurfaceView implements Callback,Runnable


获取当前mSurfaceHolder 并且把它加到CallBack回调函数中

[java]
view plaincopyprint?

SurfaceHolder mSurfaceHolder = getHolder();

mSurfaceHolder.addCallback(this);

SurfaceHolder  mSurfaceHolder = getHolder();
mSurfaceHolder.addCallback(this);


通过callBack接口监听SurfaceView的状态, 在它被创建的时候开启游戏的主线程,结束的时候销毁。这里说一下在View的构造函数中是拿不到view有关的任何信息的,因为它还没有构建好。 所以通过这个监听我们可以在surfaceCreated()中拿到当前view的属性 比如view的宽高 等等,所以callBack接口还是非常有用处的。

[java]
view plaincopyprint?

@Override
public void surfaceChanged(SurfaceHolder arg0,
int arg1, int arg2,

int arg3) {

// surfaceView的大小发生改变的时候

}

@Override
public void surfaceCreated(SurfaceHolder arg0) {

/**启动游戏主线程**/
mIsRunning = true;

mThread = new Thread(this);

mThread.start();
}

@Override
public void surfaceDestroyed(SurfaceHolder arg0) {

// surfaceView销毁的时候
mIsRunning = false;

}

@Override
public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2,
int arg3) {
// surfaceView的大小发生改变的时候

}

@Override
public void surfaceCreated(SurfaceHolder arg0) {
/**启动游戏主线程**/
mIsRunning = true;
mThread = new Thread(this);
mThread.start();
}

@Override
public void surfaceDestroyed(SurfaceHolder arg0) {
// surfaceView销毁的时候
mIsRunning = false;
}


在游戏主线程循环中在绘制开始 先拿到画布canvas 并使用mSurfaceHolder.lockCanvas()锁定画布,等绘制结束以后 使用mSurfaceHolder.unlockCanvasAndPost(mCanvas)解锁画布, 解锁画布以后画布上的内容才会显示到屏幕上。

[java]
view plaincopyprint?

@Override
public void run() {

while (mIsRunning) {

try {
Thread.sleep(100);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();
}

//在这里加上线程安全锁

synchronized (mSurfaceHolder) {

/**拿到当前画布 然后锁定**/

mCanvas =mSurfaceHolder.lockCanvas();
Draw();
/**绘制结束后解锁显示在屏幕上**/

mSurfaceHolder.unlockCanvasAndPost(mCanvas);

}
}
}

@Override
public void run() {
while (mIsRunning) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

//在这里加上线程安全锁
synchronized (mSurfaceHolder) {
/**拿到当前画布 然后锁定**/
mCanvas =mSurfaceHolder.lockCanvas();
Draw();
/**绘制结束后解锁显示在屏幕上**/
mSurfaceHolder.unlockCanvasAndPost(mCanvas);
}
}
}


由此可见SurfaceView 属于主动刷新 ,重绘过程完全是在我们自己的线程中完成 , 由于游戏中肯定会执行各种绚丽的动画效果如果使用被动刷新的View就有可能就会阻塞UI线程,所以SurfaceView 更适合做游戏。

效果图



最近有朋友反映说运行起来有点卡 我解释一下, 卡的主要原因是我的地图文件太大了,当然还有模拟器不给力的原因。我每绘制一块地图就须要使用裁剪原图,频繁的切割如此大的图片肯定会造成卡顿的情况。同学们在制作的时候将没用的地图块去掉,保留只需要的地图块这样会流畅很多喔 。

优化游戏主线程循环

同学们先看看这段代码,Draw()方法绘制结束让线程等待100毫秒在进入下一次循环。其实这样更新游戏循环是很不科学的,原因是Draw()方法每一次更新所耗费的时间是不确定的。举个例子 比如第一次循环Draw() 耗费了1000毫秒 加上线程等待100毫秒 整个循环耗时1100毫秒,第二次循环Draw() 耗时2000毫秒 加上线程等待时间100毫秒
整个循环时间就是2100毫秒。很明显这样就会造成游戏运行刷新时间时快时慢,所以说它是很不科学的。


[java]
view plaincopyprint?

public void run() {

while (mIsRunning) {
//在这里加上线程安全锁

synchronized (mSurfaceHolder) {

/**拿到当前画布 然后锁定**/

mCanvas =mSurfaceHolder.lockCanvas();
Draw();
/**绘制结束后解锁显示在屏幕上**/

mSurfaceHolder.unlockCanvasAndPost(mCanvas);

}
try {
Thread.sleep(100);
} catch (InterruptedException e) {

e.printStackTrace();
}
}
}

public void run() {
while (mIsRunning) {
//在这里加上线程安全锁
synchronized (mSurfaceHolder) {
/**拿到当前画布 然后锁定**/
mCanvas =mSurfaceHolder.lockCanvas();
Draw();
/**绘制结束后解锁显示在屏幕上**/
mSurfaceHolder.unlockCanvasAndPost(mCanvas);
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}


在贴一段科学的控游戏制循环代码,每次循环游戏主线程 在Draw()方法前后计算出Draw()方法所消耗的时间,然后在判断是否达到我们规定的刷新屏幕时间,下例是以30帧刷新一次屏幕,如果满足则继续下次循环如果不满足使用Thread.yield(); 让游戏主线程去等待 并计算当前等待时间直到等待时间满足30帧为止在继续下一次循环刷新游戏屏幕。

这里说一下Thread.yield(): 与Thread.sleep(long millis):的区别,Thread.yield():
是暂停当前正在执行的线程对象 ,并去执行其他线程。Thread.sleep(long millis):则是使当前线程暂停参数中所指定的毫秒数然后在继续执行线程。


[java]
view plaincopyprint?

/**每30帧刷新一次屏幕**/

public static
final int TIME_IN_FRAME =
30;
@Override
public void run() {

while (mIsRunning) {

/**取得更新游戏之前的时间**/

long startTime = System.currentTimeMillis();

/**在这里加上线程安全锁**/
synchronized (mSurfaceHolder) {

/**拿到当前画布 然后锁定**/
mCanvas =mSurfaceHolder.lockCanvas();
Draw();
/**绘制结束后解锁显示在屏幕上**/

mSurfaceHolder.unlockCanvasAndPost(mCanvas);
}

/**取得更新游戏结束的时间**/

long endTime = System.currentTimeMillis();

/**计算出游戏一次更新的毫秒数**/
int diffTime = (int)(endTime - startTime);

/**确保每次更新时间为30帧**/

while(diffTime <=TIME_IN_FRAME) {

diffTime = (int)(System.currentTimeMillis() - startTime);

/**线程等待**/
Thread.yield();
}

}
}

/**每30帧刷新一次屏幕**/
public static final int TIME_IN_FRAME = 30;
@Override
public void run() {
while (mIsRunning) {

/**取得更新游戏之前的时间**/
long startTime = System.currentTimeMillis();

/**在这里加上线程安全锁**/
synchronized (mSurfaceHolder) {
/**拿到当前画布 然后锁定**/
mCanvas =mSurfaceHolder.lockCanvas();
Draw();
/**绘制结束后解锁显示在屏幕上**/
mSurfaceHolder.unlockCanvasAndPost(mCanvas);
}

/**取得更新游戏结束的时间**/
long endTime = System.currentTimeMillis();

/**计算出游戏一次更新的毫秒数**/
int diffTime  = (int)(endTime - startTime);

/**确保每次更新时间为30帧**/
while(diffTime <=TIME_IN_FRAME) {
diffTime = (int)(System.currentTimeMillis() - startTime);
/**线程等待**/
Thread.yield();
}

}
}


最后由于代码较多我就不贴在博客中了 , 下面给出Demo源码的下载地址欢迎大家下载阅读互相学习,互相研究,互相讨论
雨松MOMO希望可以和大家一起进步。


下载地址:http://www.xuanyusong.com/archives/266
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐