在Android游戏当中充当主要的除了控制类外就是显示类,在J2ME中我们用Display和Canvas来实现这些,而Google Android中涉及到显示的为view类,Android游戏开发中比较重要和复杂的就是显示和游戏逻辑的处理。

      这里我们说下android.view.View和android.view.SurfaceView。SurfaceView是从View基类中派生出 来的显示类,直接子类有GLSurfaceView和VideoView,可以看出GL和视频播放以及Camera摄像头一般均使用 SurfaceView,到底有哪些优势呢?

      SurfaceView可以控制表面的格式,比如大小,显示在屏幕中的位置,最关键是的提供了SurfaceHolder类,使用getHolder方法获取,相关的有Canvas lockCanvas() 、
Canvas lockCanvas(Rect dirty) 、void removeCallback(SurfaceHolder.Callback callback)、void unlockCanvasAndPost(Canvas canvas) 控制图形以及绘制,而在SurfaceHolder.Callback 接口回调中可以通过重写下面方法实现。
      使用的SurfaceView的时候,一般情况下要对其进行创建,销毁,改变时的情况进行监视,这就要用到 SurfaceHolder.Callback.


class XxxView extends SurfaceView implements SurfaceHolder.Callback {
        public void surfaceChanged(SurfaceHolder holder,int format,int width,int height){}
        //看其名知其义,在surface的大小发生改变时激发
        public void surfaceCreated(SurfaceHolder holder){}
        //同上,在创建时激发,一般在这里调用画图的线程。
        public void surfaceDestroyed(SurfaceHolder holder) {}
        //同上,销毁时激发,一般在这里将画图的线程停止、释放。
}

      对于Surface相关的,Android底层还提供了GPU加速功能,所以一般实时性很强的应用中主要使用SurfaceView而不是直接从View构建,同时后来做android 3d OpenGL中的GLSurfaceView也是从该类实现
      SurfaceView和View最本质的区别在于,surfaceView是在一个新起的单独线程中可以重新绘制画面而View必须在UI的主线程中更新画面。
      那么在UI的主线程中更新画面 可能会引发问题,比如你更新画面的时间过长,那么你的主UI线程会被你正在画的函数阻塞。那么将无法响应按键,触屏等消息。
      当使用surfaceView 由于是在新的线程中更新画面所以不会阻塞你的UI主线程。但这也带来了另外一个问题,就是事件同步。比如你触屏了一下,你需要surfaceView中 thread处理,一般就需要有一个event queue的设计来保存touch event,这会稍稍复杂一点,因为涉及到线程同步。
      所以基于以上,根据游戏特点,一般分成两类。
      1 被动更新画面的。比如棋类,这种用view就好了。因为画面的更新是依赖于 onTouch 来更新,可以直接使用 invalidate。 因为这种情况下,这一次Touch和下一次的Touch需要的时间比较长些,不会产生影响。
      2 主动更新。比如一个人在一直跑动。这就需要一个单独的thread不停的重绘人的状态,避免阻塞main UI thread。所以显然view不合适,需要surfaceView来控制。

      Android中的SurfaceView类就是双缓冲机制。因此,开发游戏时尽量使用SurfaceView而不要使用View,这样的话效率较高,而且SurfaceView的功能也更加完善。

      考虑以上几点,所以我一直都选用 SurfaceView 来进行游戏开发。

      那么在以后源码实例中,我都会以继承surfaceView框架来进行演示。下一章将详细剖析sarfaceview ,以及附上本人写的游戏开发架构

 

 

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) 更新指定矩形区域

1 public void onDraw(Canvas canvas){
2         DosomeThing();
3         invalidate();
4 }

 

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

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

01 @Override
02 public void run() {
03     while (mIsRunning) {
04     try {
05         Thread.sleep(100);
06                 postInvalidate();
07     } catch (InterruptedException e) {
08         // TODO Auto-generated catch block
09         e.printStackTrace();
10     }
11     }
12 }

 

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 因为它的效果是最好了 当然它也是最占内存的。

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

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

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

 

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

 

01 @Override
02 protected void onDraw(Canvas canvas) {
03     /**这里先把所有须要绘制的资源绘制到mBufferBitmap上**/
04     /**绘制地图**/
05     DrawMap(mCanvas,mPaint,mBitmap);
06     /**绘制动画**/
07     RenderAnimation(mCanvas);
08     /**更新动画**/
09     UpdateAnimation();
10  
11     if(isBorderCollision) {
12     DrawCollision(mCanvas,"与边界发生碰撞");
13     }
14  
15     if(isAcotrCollision) {
16     DrawCollision(mCanvas,"与实体层发生碰撞");
17     }
18     if(isPersonCollision) {
19     DrawCollision(mCanvas,"与NPC发生碰撞");
20     }
21  
22     /**最后通过canvas一次性的把mBufferBitmap绘制到屏幕上**/
23     canvas.drawBitmap(mBufferBitmap, 0,0, mPaint);
24     super.onDraw(canvas);
25 }

 

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

 

 

2.SurfaceView


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

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

 

代码中是如何实现SurfaceView

 

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


1 public class AnimView extends SurfaceView implements Callback,Runnable

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

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

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

01 @Override
02 public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2,
03     int arg3) {
04     // surfaceView的大小发生改变的时候
05  
06 }
07  
08 @Override
09 public void surfaceCreated(SurfaceHolder arg0) {
10     /**启动游戏主线程**/
11     mIsRunning = true;
12     mThread = new Thread(this);
13     mThread.start();
14 }
15  
16 @Override
17 public void surfaceDestroyed(SurfaceHolder arg0) {
18  // surfaceView销毁的时候
19     mIsRunning = false;
20 }

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

01 @Override
02 public void run() {
03     while (mIsRunning) {
04     try {
05         Thread.sleep(100);
06     } catch (InterruptedException e) {
07         // TODO Auto-generated catch block
08         e.printStackTrace();
09     }
10  
11     //在这里加上线程安全锁
12     synchronized (mSurfaceHolder) {
13         /**拿到当前画布 然后锁定**/
14         mCanvas =mSurfaceHolder.lockCanvas();
15         Draw();
16         /**绘制结束后解锁显示在屏幕上**/
17         mSurfaceHolder.unlockCanvasAndPost(mCanvas);
18     }
19     }
20 }

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


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

优化游戏主线程循环

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

01 public void run() {
02     while (mIsRunning) {
03     //在这里加上线程安全锁
04     synchronized (mSurfaceHolder) {
05         /**拿到当前画布 然后锁定**/
06         mCanvas =mSurfaceHolder.lockCanvas();
07         Draw();
08         /**绘制结束后解锁显示在屏幕上**/
09         mSurfaceHolder.unlockCanvasAndPost(mCanvas);
10     }
11     try {
12         Thread.sleep(100);
13     } catch (InterruptedException e) {
14         e.printStackTrace();
15     }
16     }
17 }

 

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


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

01     /**每30帧刷新一次屏幕**/
02     public static final int TIME_IN_FRAME = 30;
03 @Override
04 public void run() {
05     while (mIsRunning) {
06  
07     /**取得更新游戏之前的时间**/
08     long startTime = System.currentTimeMillis();
09  
10     /**在这里加上线程安全锁**/
11     synchronized (mSurfaceHolder) {
12         /**拿到当前画布 然后锁定**/
13         mCanvas =mSurfaceHolder.lockCanvas();
14         Draw();
15         /**绘制结束后解锁显示在屏幕上**/
16         mSurfaceHolder.unlockCanvasAndPost(mCanvas);
17     }
18  
19     /**取得更新游戏结束的时间**/
20     long endTime = System.currentTimeMillis();
21  
22     /**计算出游戏一次更新的毫秒数**/
23     int diffTime  = (int)(endTime - startTime);
24  
25     /**确保每次更新时间为30帧**/
26     while(diffTime <=TIME_IN_FRAME) {
27         diffTime = (int)(System.currentTimeMillis() - startTime);
28         /**线程等待**/
29         Thread.yield();
30     }
31  
32     }
33 }

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

posted on 2013-02-26 19:48  爱哎唉  阅读(172)  评论(0)    收藏  举报