Java的强大特性让其在游戏编程和多媒体动画处理方面也毫不逊色。在Java游戏编程和动画编程中最常见的就是对于屏幕闪烁的处理。本文从J2SE的一个再现了屏幕闪烁的Java
Appilication简单动画实例展开,对屏幕闪烁的原因进行了分析,找出了闪烁成因的关键:update(Graphics g)函数对于前端屏幕的清屏。由此引出消除闪烁的方法——双缓冲。双缓冲是计算机动画处理中的传统技术,在用其他语言编程时也可以实现。本文从实例出发,着重介绍了用双缓冲消除闪烁的原理以及双缓冲在Java中的两种常用实现方法(即在update(Graphics
g)中实现和在paint(Graphics g)中实现),以期读者能对双缓冲在Java编程中的应用能有个较全面的认识。

一、问题的引入

在编写Java多媒体动画程序或用Java编写游戏程序的时候,我们得到的动画往往存在严重的闪烁(或图片断裂)。这种闪烁虽然不会给程序的效果造成太大的影响,但着实有违我们的设计初衷,也给程序的使用者造成了些许不便。闪烁到底是什么样的呢?下面的JavaApplication再现了这种屏幕闪烁的情况:

代码段一,闪烁再现

[java] view
plain
 copy

  1. import java.awt.*;
  2. import java.awt.event.*;
  3. public class DoubleBuffer extends Frame//主类继承Frame类
  4. {
  5. public paintThread pT;//绘图线程
  6. public int ypos=-80; //小圆左上角的纵坐标
  7. public DoubleBuffer()//构造函数
  8. {
  9. pT=new paintThread(this);
  10. this.setResizable(false);
  11. this.setSize(300,300); //设置窗口的首选大小
  12. this.setVisible(true); //显示窗口
  13. pT.start();//绘图线程启动
  14. }
  15. public void paint(Graphics scr) //重载绘图函数
  16. {
  17. scr.setColor(Color.RED);//设置小圆颜色
  18. scr.fillOval(90,ypos,80,80); //绘制小圆
  19. }
  20. public static void main(String[] args)
  21. {
  22. DoubleBuffer DB=new DoubleBuffer();//创建主类的对象
  23. DB.addWindowListener(new WindowAdapter()//添加窗口关闭处理函数
  24. {
  25. public void windowClosing(WindowEvent e)
  26. {
  27. System.exit(0);
  28. }});
  29. }
  30. }
  31. class paintThread extends Thread//绘图线程类
  32. {
  33. DoubleBuffer DB;
  34. public paintThread(DoubleBuffer DB) //构造函数
  35. {
  36. this.DB=DB;
  37. }
  38. public void run()//重载run()函数
  39. {
  40. while(true)//线程中的无限循环
  41. {
  42. try{
  43. sleep(30); //线程休眠30ms
  44. }catch(InterruptedException e){}
  45. DB.ypos+=5; //修改小圆左上角的纵坐标
  46. if(DB.ypos>300) //小圆离开窗口后重设左上角的纵坐标
  47. DB.ypos=-80;
  48. DB.repaint();//窗口重绘
  49. }
  50. }
  51. }

编译、运行上述例子程序后,我们会看到窗体中有一个从上至下匀速运动的小圆,但仔细观察,你会发现小圆会不时地被白色的不规则横纹隔开,即所谓的屏幕闪烁,这不是我们预期的结果。

这种闪烁是如何出现的呢?

首先我们分析一下这段代码。DoubleBuffer的对象建立后,显示窗口,程序首先自动调用重载后的paint(Graphics g)函数,在窗口上绘制了一个小圆,绘图线程启动后,该线程每隔30ms修改一下小圆的位置,然后调用repaint()函数。

注意,这个repaint()函数并不是我们重载的,而是从Frame类继承而来的。它先调用update(Graphics
g)函数,update(Graphics g)再调用paint(Graphics g)函数
。问题就出在update(Graphics
g)函数,我们来看看这个函数的源代码:

[java] view
plain
 copy

  1. public void update(Graphics g)
  2. {
  3. if (isShowing())
  4. {
  5. if (! (peer instanceof LightweightPeer))
  6. {
  7. g.clearRect(0, 0, width, height);
  8. }
  9. paint(g);
  10. }
  11. }

以上代码的意思是:(如果该组件是轻量组件的话)先用背景色覆盖整个组件,然后再调用paint(Graphics g)函数,重新绘制小圆。这样,我们每次看到的都是一个在新的位置绘制的小圆,前面的小圆都被背景色覆盖掉了。这就像一帧一帧的画面匀速地切换,以此来实现动画的效果。

但是,正是这种先用背景色覆盖组件再重绘图像的方式导致了闪烁。在两次看到不同位置小圆的中间时刻,总是存在一个在短时间内被绘制出来的空白画面(颜色取背景色)。但即使时间很短,如果重绘的面积较大的话花去的时间也是比较可观的,这个时间甚至可以大到足以让闪烁严重到让人无法忍受的地步。

另外,用paint(Graphics g)函数在屏幕上直接绘图的时候,由于执行的语句比较多,程序不断地改变窗体中正在被绘制的图象,会造成绘制的缓慢,这也从一定程度上加剧了闪烁。

就像以前课堂上老师用的旧式的幻灯机,放完一张胶片,老师会将它拿下去,这个时候屏幕上一片空白,直到放上第二张,中间时间间隔较长。当然,这不是在放动画,但上述闪烁的产生原因和这很类似。

二、问题的解决

知道了闪烁产生的原因,我们就有了更具针对性的解决闪烁的方案。已经知道update(Graphics g)是造成闪烁的主要原因,那么就从这里入手。

)尝试这样重载update(Graphics g)函数(基于代码段一修改):

[java] view
plain
 copy

  1. public void update(Graphics scr)
  2. {
  3. paint(scr);
  4. }

以上代码在重绘小圆之前没有用背景色重绘整个画面,而是直接调用paint(Graphics g)函数,这就从根本上避免了上述的那幅空白画面。

看看运行结果,闪烁果然消除了!但是更大的问题出现了,不同时刻绘制的小圆重叠在一起形成了一条线!这样的结果我们更不能接受了。为什么会这样呢?仔细分析一下,重载后的update(Graphics g)函数中没有了任何清屏的操作,每次重绘都是在先前已经绘制好的图象的基础上,当然会出现重叠的现象了。

)使用双缓冲:

这是本文讨论的重点。所谓双缓冲,就是在内存中开辟一片区域,作为后台图象,程序对它进行更新、修改,绘制完成后再显示到屏幕上。

、重载paint(Graphics g)实现双缓冲:

这种方法要求我们将双缓冲的处理放在paint(Graphics g)函数中,那么具体该怎么实现呢?先看下面的代码(基于代码段一修改):

在DoubleBuffer类中添加如下两个私有成员:

[java] view
plain
 copy

  1. private Image iBuffer;
  2. private Graphics gBuffer;
  3. //重载paint(Graphics scr)函数:
  4. public void paint(Graphics scr)
  5. {
  6. if(iBuffer==null)
  7. {
  8. iBuffer=createImage(this.getSize().width,this.getSize().height);
  9. gBuffer=iBuffer.getGraphics();
  10. }
  11. gBuffer.setColor(getBackground());
  12. gBuffer.fillRect(0,0,this.getSize().width,this.getSize().height);
  13. gBuffer.setColor(Color.RED);
  14. gBuffer.fillOval(90,ypos,80,80);
  15. scr.drawImage(iBuffer,0,0,this);
  16. }

分析上述代码:我们首先添加了两个成员变量iBuffer和gBuffer作为缓冲(这就是所谓的双缓冲名字的来历)。在paint(Graphics
scr)函数中,首先检测如果iBuffer为null,则创建一个和屏幕上的绘图区域大小一样的缓冲图象,再取得iBuffer的Graphics类型的对象的引用,并将其赋值给gBuffer,然后对gBuffer这个内存中的后台图象先用fillRect(int,int,int,int)清屏,再进行绘制操作,完成后将iBuffer直接绘制到屏幕上。

这段代码看似可以完美地完成双缓冲,但是,运行之后我们看到的还是严重的闪烁!为什么呢?回想上文所讨论的,问题还是出现在update(Graphics g)函数!这段修改后的程序中的update(Graphics
g)函数还是我们从父类继承的。在update(Graphics g)中,clearRect(int,int,int,int)对前端屏幕进行了清屏操作,而在paint(Graphics
g))中的方法重载update(Graphics g)即可:

[java] view
plain
 copy

  1. public void update(Graphics scr)
  2. {
  3. paint(scr);
  4. }

)中没有了清屏操作,消除闪烁的同时严重破坏了动画效果,这里我们把清屏操作放在了后台图象上,消除了闪烁的同时也获得了预期的动画效果。

、重载update(Graphics g)实现双缓冲:

这是比较传统的做法。也是实际开发中比较常用的做法。我们看看实现这种方法的代码(基于代码段一修改):

在DoubleBuffer类中添加如下两个私有成员:

[java] view
plain
 copy

  1. private Image iBuffer;
  2. private Graphics gBuffer;
  3. //重载paint(Graphics scr)函数:
  4. public void paint(Graphics scr)
  5. {
  6. scr.setColor(Color.RED);
  7. scr.fillOval(90,ypos,80,80);
  8. }

重载update(Graphics scr)函数:

[java] view
plain
 copy

  1. public void update(Graphics scr)
  2. {
  3. if(iBuffer==null)
  4. {
  5. iBuffer=createImage(this.getSize().width,this.getSize().height);
  6. gBuffer=iBuffer.getGraphics();
  7. }
  8. gBuffer.setColor(getBackground());
  9. gBuffer.fillRect(0,0,this.getSize().width,this.getSize().height);
  10. paint(gBuffer);
  11. scr.drawImage(iBuffer,0,0,this);
  12. }

分析上述代码:我们把对后台图象的创建、清屏以及重绘等一系列动作都放在了update(Graphicsscr)函数中,而paint(Graphics g)函数只是负责绘制什么样的图象,以及怎样绘图,函数的最后实现了后台图象向前台绘制的过程。

运行上述修改后的程序,我们会看到完美的消除闪烁后的动画效果。就像在电影院看电影,每张胶片都是在后台准备好的,播放完一张胶片之后,下一张很快就被播放到前台,自然不会出现闪烁的情形。

为了让读者能对双缓冲有个全面的认识现将上述双缓冲的实现概括如下:

)定义一个Graphics对象gBuffer和一个Image对象iBuffer。按屏幕大小建立一个缓冲对象给iBuffer。然后取得iBuffer的Graphics赋给gBuffer。此处可以把gBuffer理解为逻辑上的缓冲屏幕,而把iBuffer理解为缓冲屏幕上的图象。

)在gBuffer(逻辑上的屏幕)上用paint(Graphics
g)函数绘制图象。

)将后台图象iBuffer绘制到前台。

以上就是一次双缓冲的过程。注意,将这个过程联系起来的是repaint()函数。paint(Graphics g)是一个系统调用语句,不能由程序员手工调用。只能通过repaint()函数调用。

三、问题的扩展

、关于闪烁的补充:

其实引起闪烁的不仅仅是上文提到的那样,多种物理因素也可以引起闪烁,无论是CRT显示器还是LCD显示器都存在闪烁的现象。本文只讨论软件编程引起的闪烁。但是即使双缓冲做得再好,有时也是会有闪烁,这就是硬件方面的原因了,我们只能修改程序中的相关参数来降低闪烁(比如让画面动得慢一点),而不是编程方法的问题。

、关于消除闪烁的方法的补充:

上文提到的双缓冲的实现方法只是消除闪烁的方法中的一种。如果在swing中,组件本身就提供了双缓冲的功能,我们只需要进行简单的函数调用就可以实现组件的双缓冲,在awt中却没有提供此功能。另外,一些硬件设备也可以实现双缓冲,每次都是先把图象画在缓冲中,然后再绘制在屏幕上,而不是直接绘制在屏幕上,基本原理还是和文中的类似的。还有其他用软件实现消除闪烁的方法,但双缓冲是个简单的、值得推荐的方法。

、关于双缓冲的补充:

双缓冲技术是编写J2ME游戏的关键技术之一。双缓冲付出的代价是较大的额外内存消耗。但现在节省内存已经不再是程序员们考虑的最首要的问题了,游戏的画面在游戏制作中是至关重要的,所以以额外的内存消耗换取程序质量的提高还是值得肯定的。

、双缓冲的改进:

有时动画中相邻的两幅画面只是有很少部分的不同,这就没必要每次都对整个绘图区进行清屏。我们可以对文中的程序进行修改,使之每次只对部分屏幕清屏,这样既能节省内存,又能减少绘制图象的时间,使动画更加连贯!

java的双缓冲技术的更多相关文章

  1. Java中用双缓冲技术消除闪烁

    在Java编写具有连贯变化的窗口程序时,通常的办法是在子类中覆盖父类的paint(Graphics)方法,在方法中使用GUI函数实现窗口重绘的过程.连贯变换的窗口会不断地调用update(Graphi ...

  2. Android开发之用双缓冲技术绘图

    双缓冲技术主要用在画图,动画效果上,其原理就是:将资源先载入到缓冲区,然后再将缓冲区整个载入到View上面去. 双缓冲技术可以有效防止闪烁,提高显示质量. DrawView.java: package ...

  3. OpenGL中实现双缓冲技术

    在OpenGL中实现双缓冲技术的一种简单方法: 1.在调用glutInitDisplayMode函数时, 开启GLUT_DOUBLE,即glutInitDisplayMode(GLUT_RGB | G ...

  4. 《MFC游戏开发》笔记六 图像双缓冲技术:实现一个流畅的动画

    本系列文章由七十一雾央编写,转载请注明出处.  http://blog.csdn.net/u011371356/article/details/9334121 作者:七十一雾央 新浪微博:http:/ ...

  5. Win32 GDI 非矩形区域剪裁,双缓冲技术

    传统的Win32通过GDI提供图形显示的功能,包括了基本的绘图功能,如画线.方块.椭圆等等,高级功能包括了多边形和Bezier的绘制.这样app就不用关心那些图形学的细节了,有点类似于UNIX上的X- ...

  6. 双缓冲技术(Double Buffering)(1、简介和源代码部分)

    这一节实在是有些长,翻译完后统计了一下,快到2w字了.考虑到阅读的方便和网络的速度,打算把这节分为5个部分,第一部分为双缓冲技术的一个 简介和所有的代码,如果能够看懂代码,不用看译文也就可以了.第二部 ...

  7. avalon与双缓冲技术

    avalon与双缓冲技术 avalon1.5一个重要技术升级是引进异步渲染.异步渲染在游戏界有一个更专业的名字,叫双缓冲.游戏界要刷新界面与我们刷新浏览器视图,面临的问题是一致的.视图是由许多存在套嵌 ...

  8. C#中利用双缓冲技术解决绘图闪屏问题。

    这段时间在做一个小型游戏,在界面显示的时候用到了一些图形.一开始涉及到的图形全都用控件的背景图片代替了.这样游戏运行的时候存在的一个很大的问题是游戏运行很慢.小组成员费尽周折,即将放弃,每一个成员都愁 ...

  9. c++双缓冲技术,以避免闪烁绘图

    当数据量非常大时,画图可能须要几秒钟甚至更长的时间,并且有时还会出现闪烁现象,为了解决这些问题.可採用双缓冲技术来画图. 双缓冲即在内存中创建一个与屏幕画图区域一致的对象,先将图形绘制到内存中的这个对 ...

随机推荐

  1. 全文检索Lucene (2)

    接着全文检索Lucene (1) . 下面我们来深入的研究一下,如何使用Lucene! 从全文检索Lucene (1)中我们可以看出,Lucene就好比一个双向的工作流,一方面是对索引库的维护,另一方 ...

  2. Markdown-----Markdown使用文档

    最近才接触Markdown,为了快速记忆,整理了这个文档,欢迎补充. Markdown和扩展Markdown简洁的语法 代码块高亮 图片链接和图片上传 LaTex数学公式 UML序列图和流程图 离线写 ...

  3. 集合框架之List接口

    有序的 collection(也称为序列).此接口的用户可以对列表中每个元素的插入位置进行精确地控制.用户可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素. 与 set 不同,列表 ...

  4. 14 fragment传值

    两个fragment传值 方式一 布局文件代码: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/and ...

  5. 【并发编程】AIDL关键字

    oneway Oneway interfaces In early betas, the Android IPC was strictly synchronous. This means that s ...

  6. linux 定时任务详解 按秒设定

    实现linux定时任务有:cron.anacron.at等,这里主要介绍cron服务. 名词解释: cron是服务名称,crond是后台进程,crontab则是定制好的计划任务表. 软件包安装: 要使 ...

  7. Cocos2D iOS之旅:如何写一个敲地鼠游戏(四):创建TexturePacker自动脚本

    大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请告诉我,如果觉得不错请多多支持点赞.谢谢! hopy ;) 免责申明:本博客提供的所有翻译文章原稿均来自互联网,仅供学习交流 ...

  8. Android性能优化之加快应用启动速度

    应用的启动 启动方式 通常来说,在安卓中应用的启动方式分为两种:冷启动和热启动. 1.冷启动:当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用,这个启动方式就是冷启动. ...

  9. Andriod的国际化-android学习之旅(五十八)

    android资源国际化

  10. 【IOS 开发】Object-C 运算符

    博客地址 : http://blog.csdn.net/shulianghan/article/details/41624613 1. 算术运算符 算术运算符 : 加(+), 减(-), 乘(*), ...