这段时间在看Android游戏编程,感觉SurfaceView这个框架挺好的。分享一下:
1.建立一个工程,在该工程下自定义一个类“MySurfaceView”,此类继承SurfaceView,除此以外还要实现android.view.SurfaceHolder.Callback接口,代码如下:
public class MySurfaceView extends SurfaceView implements Callback{ //用于控制SurfaceView private SurfaceHolder sfh; //声明一个画笔 private Paint paint; //文本的坐标 private int textX = 10,textY = 10; //声明一个线程 private Thread th; //线程消亡的标识位 private boolean flag; //声明一个画布 private Canvas canvas; //声明屏幕的宽高 private int screenW,screenH; /** *SurfaceView初始化函数 */ public MySurfaceView(Context context){ super(context); //实例SurfaceHolder sfh = this.getHolder(); // 为SurfaceView添加状态监听 sfh.addCallback(this); //实例一个画笔 paint = new Paint(); //设置画笔颜色为白色 paint.setColor(Color.WHITE); //设置焦点 setFocusable(true); } @Override public void surfaceCreate(SurfaceHolder holer){ screenW = this.getWidth(); screenH = this.getHeight(); flag = true; //实例化线程 th = new Thread(this); //启动线程 th.start(); } @Override public void surfaceChanged(SurfaceHolder holder,int format,int width,int height){ } @Override public void surfaceDestroyed(SurfaceHolder holder){ } public myDraw(){ try{ Canvas canvas = sfh.lockCanvas(); if(canvas != null){ canvas.drawRGB(0,0,0); canvas.drawText("Game",textX,textY,paint);} }catch(Exception e){ //TODO: handle exception }finally{ if(canvas != null) sfh.unlockCanvasAndPost(canvas); } } } /** *触摸屏监听 */ @Override public boolean onTouchEvent(MotionEvent event){ textX = (int) event.getX(); textY = (int) event.getY(); return true; } /** *按键事件监听 */ @Override public boolean onKeyDown(int keyCode,KeyEvent event){ return super.onkeyDown(keyCode,event); } /** *游戏逻辑 */ private void logic(){ } @Override public void run(){ while(flag){ long start = System.currentTimeMills(); myDraw(); logic(); long end = System.currentTimeMills(); try{ if(end - start < 50){ Thread.sleep(50-(end - start)); } }catch(InterruptedException e){ e.printStackTrace(); } } } }
(1)线程标识位
在代码中“boolean flag;”语句声明一个布尔值,它主要用于以下两点;
1.1便于消亡线程
大家知道一个线程一旦启动,就会执行其run()函数,run()函数执行结束后,线程也伴随着消亡。由于游戏开发中使用的线程一般都会在run()函数中使用一个while死循环,在这个循环会调用绘图和逻辑函数,使得不断的刷新画布和更新逻辑,那么如果游戏暂停或者游戏结束时,为了便于销毁线程在此设置一个标识位来控制。
1.2防止重复创建线程及异常
为什么会重复创建线程,首先从Android系统的手机说起。熟悉或者接触过Android系统的人都知道,android手机上一般都会有“Back (返回)”与Home(小房子)按键;
不管当前手机运行的是什么程序,只要单击back或者home按键的时候,默认会将当前的程序切入到系统后台运行(程序中没有截获这两个按钮的前提下);也正是因为如此,会造成SurfaceView视图的状态的发生改变。下面来讲解这两个按钮按下以及重新回到程序时SurfaceView都执行到了哪些函数。
首先单击back按钮当前程序切入后台,然后单击项目重新回到程序中,SurfaceView的状态变化为:surfaceDestroyed--->构造函数-->surfaceCreated--->surfaceChanged.
然后单击“Home”按钮使当前程序切入后台,然后单击项目重新回到程序中,SurfaceView的状态变化为:surfaceDestroyed--->surfaceCreated--->surfaceChanged.
通过SurfaceView的状态变化可以很明显的看到,但点击back按键并重新进入程序的过程要比home按键多执行一个构造函数,也就是说,当点击“back”返回按键时,SurfaceView视图会被重新加载。
正是因为这个原因,如果线程的初始化是在视图的构造函数或者在视图构造函数之前,那么线程启动也要放在视图构造函数中进行。
千万不要把线程的初始化放在surfaceCreated视图创建函数之前,而线程的启动却放在surfaceCreated视图创建的函数中,否则程序一旦被玩家点击home按键后再重新回到游戏时,程序会抛出ILLegalThreadStateException:Thread already started;
异常是因为线程已经启动造成的,原因很简单,因为程序被home键切入后台再从后台回复时,就会直接进入surfaceCreated视图创建函数中,又执行了一遍线程启动!
能够想到的解决方法是,可以将线程的初始化和启动都放到视图的构造函数中,或者都放在视图的创建函数中,但是又会有新的问题,如果将线程的初始化和启动都放在视图的构造函数中,那么程序被back键切入后台再从后台恢复时,线程的数量会增多,反复多次,就会反复多出对应的线程。
那么大家可能又会想到将flag这个线程的标识符在视图摧毁时让其值改为false,从而使当前这个线程的run方法执行完毕,以达到摧毁掉线程的目的,不幸的是,这也是错误的,大家想想,即使在视图销毁时利用flag标识位摧毁游戏线程,但是如果点击home键,当程序恢复时,程序就不在执行了,也就是说重绘和逻辑函数都不在执行。
所以最完美的做法就是:线程的初始化与线程的启动都写在视图的surfaceCreated创建函数中,并将线程标识位在视图摧毁时将其值改为false,这样既可以避免线程已经启动异常,还可以避免点击back按键无限增加线程数的问题。
2获取视图的宽和高
在SurfaceView视图中获取视图的宽和高的方法:
this.getWidth();获取视图宽度。
this.getHeight();获取视图高度。
在SurfaceView视图中获取视图的宽高,一定要在视图创建之后才可以获取到,也就是在surfaceCreated函数之后获取,在此函数执行之前获取到的永远是零,因为当前视图还没有创建,是没有宽高值的。
(3)绘图try一下
因为当SurfaceView不可编辑或者尚未创建时,调用lockCanvas()函数返回null,Canvas进行绘图时也会出现不可预知的问题,所以要对绘制函数中进行try....catch处理;既然lockCanvas函数有可能获取为null,那么为了避免其他使用canvas实例进行绘制的函数报错,在使用Canvas开始绘制时,需要对其进行判定是否为null。
(4)提交画布必须放在finally中
绘图的时候可能会出现不可预知的Bug,虽然使用try语句包起来了,不会导致程序崩溃;但是一旦在提交画布之前出错,那么解锁提交画布函数则无法被执行到,这样会导致下次通过lockCanvas来获取Canvas时程序抛出异常,原因是因为画布上次没有解锁提交!所以画布将解锁提交的函数应放在fanally语句块中。
还要注意,虽然这样保证了每次能正常提交解锁画布,但是提交解锁之前要保证画布不为空的前提,所以还需要判断Canvas是否为空,这样一来就完美了。
(5)刷帧时间尽可能保证一致
虽然在线程循环中,设置了休眠时间,但是这样并不完善,比如当前项目中,run的while循环中除了调用绘图函数还一直调用处理游戏逻辑的logic()函数,虽然在当前项目的逻辑函数中并没有写任何的代码,但是假设这个逻辑函数logic()中写了几千行逻辑,那么系统在处理逻辑时,时间的开销是否与上次的相同,这是无法预料的,但是可以尽可能地让其时间差值趋于相同。假设游戏线程的休眠时间为X毫秒,一般线程的休眠写法为:
Thread.sleep(x);
优化写法步骤如下:
步骤一:
首先通过系统获取到一个事件戳;
步骤二:
处理以上所有的函数之后,再次通过系统函数获取一个时间戳;
步骤三:
通过两个时间戳的差值,就可以知道这些函数所消耗的时间;如果end - start > X,那线程就完全没有必要去休眠;如果end - start < X ,那线程的休眠时间应该为:X - (end - start).
一般游戏中刷新时间在50~100毫秒之间,也就是每秒10~20帧左右;当然还要视具体情况和项目而定。