java 物體運動過程中閃爍問題淺談
Java雙緩衝技術
Java的強大特性讓其在遊戲程式設計和多媒體動畫處理方面也毫不遜色。在Java遊戲程式設計和動畫程式設計中最常見的就是對於螢幕閃爍的處理。本文從J2SE的一個再現了螢幕閃爍的Java Appilication簡單動畫例項展開,對螢幕閃爍的原因進行了分析,找出了閃爍成因的關鍵:update(Graphics g)函式對於前端螢幕的清屏。由此引出消除閃爍的方法——雙緩衝。雙緩衝是計算機動畫處理中的傳統技術,在用其他語言程式設計時也可以實現。本文從例項出發,著重介紹了用雙緩衝消除閃爍的原理以及雙緩衝在Java中的兩種常用實現方法(即在update(Graphics g)中實現和在paint(Graphics g)中實現),以期讀者能對雙緩衝在Java程式設計中的應用能有個較全面的認識。
一、問題的引入
在編寫Java多媒體動畫程式或用Java編寫遊戲程式的時候,我們得到的動畫往往存在嚴重的閃爍(或圖片斷裂)。這種閃爍雖然不會給程式的效果造成太大的影響,但著實有違我們的設計初衷,也給程式的使用者造成了些許不便。閃爍到底是什麼樣的呢?下面的JavaApplication再現了這種螢幕閃爍的情況:
程式碼段一,閃爍再現
import java.awt.*; import java.awt.event.*; public class DoubleBuffer extends Frame//主類繼承Frame類 { public paintThread pT;//繪圖執行緒 public int ypos=-80; //小圓左上角的縱座標 public DoubleBuffer()//建構函式 { pT=new paintThread(this); this.setResizable(false); this.setSize(300,300); //設定視窗的首選大小 this.setVisible(true); //顯示視窗 pT.start();//繪圖執行緒啟動 } public void paint(Graphics scr) //過載繪圖函式 { scr.setColor(Color.RED);//設定小圓顏色 scr.fillOval(90,ypos,80,80); //繪製小圓 } public static void main(String[] args) { DoubleBuffer DB=new DoubleBuffer();//建立主類的物件 DB.addWindowListener(new WindowAdapter()//新增視窗關閉處理函式 { public void windowClosing(WindowEvent e) { System.exit(0); } }); } } class paintThread extends Thread//繪圖執行緒類 { DoubleBuffer DB; public paintThread(DoubleBuffer DB) //建構函式 { this.DB=DB; } public void run()//過載run()函式 { while(true)//執行緒中的無限迴圈 { try{ sleep(30); //執行緒休眠30ms }catch(InterruptedException e){} DB.ypos+=5; //修改小圓左上角的縱座標 if(DB.ypos>300) //小圓離開視窗後重設左上角的縱座標 DB.ypos=-80; DB.repaint();//視窗重繪 } } }
編譯、執行上述例子程式後,我們會看到窗體中有一個從上至下勻速運動的小圓,但仔細觀察,你會發現小圓會不時地被白色的不規則橫紋隔開,即所謂的螢幕閃爍,這不是我們預期的結果。
這種閃爍是如何出現的呢?
首先我們分析一下這段程式碼。DoubleBuffer的物件建立後,顯示視窗,程式首先自動呼叫過載後的paint(Graphics g)函式,在視窗上繪製了一個小圓,繪圖執行緒啟動後,該執行緒每隔30ms修改一下小圓的位置,然後呼叫repaint()函式。
注意,這個repaint()函式並不是我們過載的,而是從Frame類繼承而來的。它先呼叫update(Graphics g)函式,update(Graphics g)再呼叫paint(Graphics g)函式。問題就出在update(Graphics g)函式,我們來看看這個函式的原始碼:
public void update(Graphics g)
{
if (isShowing())
{
if (! (peer instanceof LightweightPeer))
{
g.clearRect(0, 0, width, height);
}
paint(g);
}
}
以上程式碼的意思是:(如果該元件是輕量元件的話)先用背景色覆蓋整個元件,然後再呼叫paint(Graphics g)函式,重新繪製小圓。這樣,我們每次看到的都是一個在新的位置繪製的小圓,前面的小圓都被背景色覆蓋掉了。這就像一幀一幀的畫面勻速地切換,以此來實現動畫的效果。
但是,正是這種先用背景色覆蓋元件再重繪影象的方式導致了閃爍。在兩次看到不同位置小圓的中間時刻,總是存在一個在短時間內被繪製出來的空白畫面(顏色取背景色)。但即使時間很短,如果重繪的面積較大的話花去的時間也是比較可觀的,這個時間甚至可以大到足以讓閃爍嚴重到讓人無法忍受的地步。
另外,用paint(Graphics g)函式在螢幕上直接繪圖的時候,由於執行的語句比較多,程式不斷地改變窗體中正在被繪製的圖象,會造成繪製的緩慢,這也從一定程度上加劇了閃爍。
就像以前課堂上老師用的舊式的幻燈機,放完一張膠片,老師會將它拿下去,這個時候螢幕上一片空白,直到放上第二張,中間時間間隔較長。當然,這不是在放動畫,但上述閃爍的產生原因和這很類似。
二、問題的解決
知道了閃爍產生的原因,我們就有了更具針對性的解決閃爍的方案。已經知道update(Graphics g)是造成閃爍的主要原因,那麼就從這裡入手。
(1)嘗試這樣過載update(Graphics g)函式(基於程式碼段一修改):
public void update(Graphics scr)
{
paint(scr);
}
以上程式碼在重繪小圓之前沒有用背景色重繪整個畫面,而是直接呼叫paint(Graphics g)函式,這就從根本上避免了上述的那幅空白畫面。
看看執行結果,閃爍果然消除了!但是更大的問題出現了,不同時刻繪製的小圓重疊在一起形成了一條線!這樣的結果我們更不能接受了。為什麼會這樣呢?仔細分析一下,過載後的update(Graphics g)函式中沒有了任何清屏的操作,每次重繪都是在先前已經繪製好的圖象的基礎上,當然會出現重疊的現象了。
2)使用雙緩衝:
這是本文討論的重點。所謂雙緩衝,就是在記憶體中開闢一片區域,作為後臺圖象,程式對它進行更新、修改,繪製完成後再顯示到螢幕上。
1、過載paint(Graphics g)實現雙緩衝:
這種方法要求我們將雙緩衝的處理放在paint(Graphics g)函式中,那麼具體該怎麼實現呢?先看下面的程式碼(基於程式碼段一修改):
在DoubleBuffer類中新增如下兩個私有成員:
private Image iBuffer;
private Graphics gBuffer;
//過載paint(Graphics scr)函式:
public void paint(Graphics scr)
{
if(iBuffer==null)
{
iBuffer=createImage(this.getSize().width,this.getSize().height);
gBuffer=iBuffer.getGraphics();
}
gBuffer.setColor(getBackground());
gBuffer.fillRect(0,0,this.getSize().width,this.getSize().height);
gBuffer.setColor(Color.RED);
gBuffer.fillOval(90,ypos,80,80);
scr.drawImage(iBuffer,0,0,this);
}
分析上述程式碼:我們首先添加了兩個成員變數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)中,對後臺圖象又進行了清屏操作。那麼如果保留後臺清屏,去掉多餘的前臺清屏應該就會消除閃爍。所以,我們只要按照(1)中的方法過載update(Graphics g)即可:
public void update(Graphics scr)
{
paint(scr);
}
這樣就避開了對前端圖象的清屏操作,避免了螢幕的閃爍。雖然和(1)中用一樣的方法過載update(Graphics g),但(1)中沒有了清屏操作,消除閃爍的同時嚴重破壞了動畫效果,這裡我們把清屏操作放在了後臺圖象上,消除了閃爍的同時也獲得了預期的動畫效果。
2、過載update(Graphics g)實現雙緩衝:
這是比較傳統的做法。也是實際開發中比較常用的做法。我們看看實現這種方法的程式碼(基於程式碼段一修改):
在DoubleBuffer類中新增如下兩個私有成員:
private Image iBuffer;
private Graphics gBuffer;
//過載paint(Graphics scr)函式:
public void paint(Graphics scr)
{
scr.setColor(Color.RED);
scr.fillOval(90,ypos,80,80);
}
//過載update(Graphics scr)函式:
public void update(Graphics scr)
{
if(iBuffer==null)
{
iBuffer=createImage(this.getSize().width,this.getSize().height);
gBuffer=iBuffer.getGraphics();
}
gBuffer.setColor(getBackground());
gBuffer.fillRect(0,0,this.getSize().width,this.getSize().height);
paint(gBuffer);
scr.drawImage(iBuffer,0,0,this);
}
分析上述程式碼:我們把對後臺圖象的建立、清屏以及重繪等一系列動作都放在了update(Graphicsscr)函式中,而paint(Graphics g)函式只是負責繪製什麼樣的圖象,以及怎樣繪圖,函式的最後實現了後臺圖象向前臺繪製的過程。
執行上述修改後的程式,我們會看到完美的消除閃爍後的動畫效果。就像在電影院看電影,每張膠片都是在後臺準備好的,播放完一張膠片之後,下一張很快就被播放到前臺,自然不會出現閃爍的情形。
為了讓讀者能對雙緩衝有個全面的認識現將上述雙緩衝的實現概括如下:
(1)定義一個Graphics物件gBuffer和一個Image物件iBuffer。按螢幕大小建立一個緩衝物件給iBuffer。然後取得iBuffer的Graphics賦給gBuffer。此處可以把gBuffer理解為邏輯上的緩衝螢幕,而把iBuffer理解為緩衝螢幕上的圖象。
(2)在gBuffer(邏輯上的螢幕)上用paint(Graphics g)函式繪製圖象。
(3)將後臺圖象iBuffer繪製到前臺。
以上就是一次雙緩衝的過程。注意,將這個過程聯絡起來的是repaint()函式。paint(Graphics g)是一個系統呼叫語句,不能由程式設計師手工呼叫。只能通過repaint()函式呼叫。
三、問題的擴充套件
1、關於閃爍的補充:
其實引起閃爍的不僅僅是上文提到的那樣,多種物理因素也可以引起閃爍,無論是CRT顯示器還是LCD顯示器都存在閃爍的現象。本文只討論軟體程式設計引起的閃爍。但是即使雙緩衝做得再好,有時也是會有閃爍,這就是硬體方面的原因了,我們只能修改程式中的相關引數來降低閃爍(比如讓畫面動得慢一點),而不是程式設計方法的問題。
2、關於消除閃爍的方法的補充:
上文提到的雙緩衝的實現方法只是消除閃爍的方法中的一種。如果在swing中,元件本身就提供了雙緩衝的功能,我們只需要進行簡單的函式呼叫就可以實現元件的雙緩衝,在awt中卻沒有提供此功能。另外,一些硬體裝置也可以實現雙緩衝,每次都是先把圖象畫在緩衝中,然後再繪製在螢幕上,而不是直接繪製在螢幕上,基本原理還是和文中的類似的。還有其他用軟體實現消除閃爍的方法,但雙緩衝是個簡單的、值得推薦的方法。
2、關於雙緩衝的補充:
雙緩衝技術是編寫J2ME遊戲的關鍵技術之一。雙緩衝付出的代價是較大的額外記憶體消耗。但現在節省記憶體已經不再是程式設計師們考慮的最首要的問題了,遊戲的畫面在遊戲製作中是至關重要的,所以以額外的記憶體消耗換取程式質量的提高還是值得肯定的。
3、雙緩衝的改進:
有時動畫中相鄰的兩幅畫面只是有很少部分的不同,這就沒必要每次都對整個繪圖區進行清屏。我們可以對文中的程式進行修改,使之每次只對部分螢幕清屏,這樣既能節省記憶體,又能減少繪製圖象的時間,使動畫更加連貫!