版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
http://www.blogbus.com/oveplayer-logs/88073442.html
http://www.blogbus.com/oveplayer-logs/88073442.html
SurfaceView在Android中用作游戏开发是最适宜的,本文就将演示游戏开发中常用的两种绘图刷新策略在SurfaceView中的实现方法。
首先我们来看一下本例需要用到的两个素材图片:
question.png是一个半透明的图像,我们希望将它放在上面,围绕其圆心不断旋转。
实现代码如下:
package SkyD.SurfaceViewTest; import android.app.Activity; import android.content.Context; import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.graphics.Canvas;import android.graphics.Matrix;import android.graphics.Paint;import android.os.Bundle;import android.view.SurfaceHolder;import android.view.SurfaceView; public class Main extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(new MySurfaceView(this)); } // 自定义的SurfaceView子类 class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback { // 背景图 private Bitmap BackgroundImage; // 问号图 private Bitmap QuestionImage; SurfaceHolder Holder; public MySurfaceView(Context context) { super(context); BackgroundImage = BitmapFactory.decodeResource(getResources(), R.drawable.bg); QuestionImage = BitmapFactory.decodeResource(getResources(), R.drawable.question); Holder = this.getHolder();// 获取holder Holder.addCallback(this); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // TODO Auto-generated method stub } @Override public void surfaceCreated(SurfaceHolder holder) { // 启动自定义线程 new Thread(new MyThread()).start(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { // TODO Auto-generated method stub } // 自定义线程类 class MyThread implements Runnable { @Override public void run() { Canvas canvas = null; int rotate = 0;// 旋转角度变量 while (true) { try { canvas = Holder.lockCanvas();// 获取画布 Paint mPaint = new Paint(); // 绘制背景 canvas.drawBitmap(BackgroundImage, 0, 0, mPaint); // 创建矩阵以控制图片旋转和平移 Matrix m = new Matrix(); // 设置旋转角度 m.postRotate((rotate += 48) % 360, QuestionImage.getWidth() / 2, QuestionImage.getHeight() / 2); // 设置左边距和上边距 m.postTranslate(47, 47); // 绘制问号图 canvas.drawBitmap(QuestionImage, m, mPaint); // 休眠以控制最大帧频为每秒约30帧 Thread.sleep(33); } catch (Exception e) { } finally { Holder.unlockCanvasAndPost(canvas);// 解锁画布,提交画好的图像 } } } } } }
模拟器中的运行效果:
这看起来不错,但是有一个问题:我们在代码中设置的帧频最大值是每秒30帧,而实际运行时的帧频根据目测就能看出是到不了30帧的,这是因为程序在每一帧都要对整个画面进行重绘,过多的时间都被用作绘图处理,所以难以达到最大帧频。
脏矩形刷新
接下来我们将采取脏矩形刷新的方法来优化性能,所谓脏矩形刷新,意为仅刷新有新变化的部分所在的矩形区域,而其他没用的部分就不去刷新,以此来减少资源浪费。
我们可以通过在获取Canvas画布时,为其指派一个参数来声明我们需要画布哪个局部,这样就可以只获得这个部分的控制权:
不过别高兴的太早,实际上如果把刷新区域扩大到整个问号图形所在的矩形区域的话,你会发现优化作用变得微乎其微了,还是没法达到最大帧频的,因为更新区域增大了3倍,带来的资源消耗也就大幅增加。
覆盖刷新
这种情况下就应当考虑结合覆盖刷新方法再进一步优化了。
试想一下,我们每次刷新时最大的消耗在哪?
没错,在背景图绘制上,这个绘制区域非常大,会消耗我们很多资源,但实际上背景图在此例中是从不变化的,也就是说我们浪费了很多资源在无用的地方。
那么可不可以只绘制一次背景,以后每次都只绘制会动的问号图形呢?
完全可以,尝试修改一下代码,再前面加一个帧计数器,然后我们仅在第一帧的时候绘制背景:
啊哈,这正是我使用半透明图案做范例的目的,通过这个重影,我们就能看出,覆盖刷新其实就是将每次的新的图形绘制到上一帧去,所以如果图像是半透明的,就要考虑重复叠加导致的问题了,而如果是完全不透明的图形则不会有任何问题。
背景会在背景图和黑色背景之间来回闪。
这个问题其实是源于SurfaceView的双缓冲机制,我理解就是它会缓冲前两帧的图像交替传递给后面的帧用作覆盖,这样由于我们仅在第一帧绘制了背景,第二帧就是无背景状态了,且通过双缓冲机制一直保持下来,解决办法就是改为在前两帧都进行背景绘制:
结语
我这也是刚接触Android开发,分享这点心得出来,有写的不对的欢迎指点一二^^
此文是针对上一篇《Android SurfaceView 绘图覆盖刷新及脏矩形刷新方法》的修正。
帧频处理
今天我在审视上篇示例代码时猛然发现我犯了个低级错误,致使帧频达不到预期,这个错误在这里:
这里设置每次绘制之后固定休眠33毫秒,以此来限制每秒帧频在30帧。
但实际上这里忽视了绘图及其他运算所消耗的时间,也就是说除非其他所有操作的总耗时都不足1/3毫秒,否则我们就根本无法达到每秒30帧的期望。
修正方法是在每帧开始处理前获取系统当前时间值,然后在处理完毕后再获取一次当前时间值,然后用当前值减去旧值,就得到了处理所消耗的时间,然后用33毫秒减去得到的消耗时间,就是我们此帧接下来应当休眠的时间值了:
现在运行一下程序,就能看到跑得很流畅了。
我想可能从事过游戏开发的人都能一眼看出这个错误吧,我是没经验在这栽了一下>_<~
绘图处理
在绘图处理时,还有一点需要注意的就是:
锁定Canvas之后到解锁这段时间内,最好仅执行与绘图有关的操作,其他代码越少越好,就好像往常我们开发时打开数据库链接一样。
因为我们锁定后其他线程如果要使用的话就得等待了,所以尽快释放会节省其他线程的等待时间,要尽量把无关紧要的代码放在这个时间段以外执行。
针对此例而言,就是那个有关矩阵部分的操作可以调整到前面去执行,Paint对象也可以挪到前面去创建,而且不需要每次循环都创建一个Paint对象,创建一次就足矣:
经过调整之后,绿色高亮部分就只剩下绘图相关的代码了,减负了不少呵。