J2ME遊戲優化祕密

本文描述了代碼優化在爲移動設備寫運行起來速度快的遊戲中扮演的角色。我會用例子說明如何、什麼時候和爲什麼要優化你的代碼,來榨乾兼容MIDP的手機的 每一滴性能。我們將要討論爲什麼優化是必要的和爲什麼有時候最好不要優化。我將解釋高級優化和低級優化的差別,然後我們會知道如何使用J2ME無線開發包 (WTK)自帶的Profile程序來發現到哪裏去優化你的代碼。這篇文章最後揭示了很多讓你的MIDlet運行的技術。

爲什麼優化?

計算機遊戲可以分爲兩大類: 實時的和輸入驅動的。 輸入驅動的遊戲顯示遊戲的當前運行狀態,並在繼續之前無限地等待用戶的輸入。

撲克牌遊戲就屬於這一類,同樣,大多數的猜謎遊戲、過關遊戲和文字冒險遊戲都屬於這一類。實時遊戲,有時候被稱爲技能或動作遊戲,不等待用戶,他們不停地運行直到遊戲結束。

技能和動作遊戲經常以大量的屏幕上運東爲特徵(想想Galaga遊戲和Robotron遊戲)。刷新率必須至少有10fps(每秒的幀數)並且要有足夠的 動作來保持玩家的挑戰性。它們需要玩家快速的反應和好的手眼配合,所以就強迫S&A(技能和動作)遊戲必須對玩家的輸入有很強的響應能力。在快速 響應玩家案件的同時提供高幀數的圖形動作,這是實時遊戲的代碼必須運行起來快的原因。在用J2ME開發的時候,挑戰性就更大了。

Java 2 Micro Edition(J2ME)是java的一個分解版本。 適用於有限功能的小型設備,比如手機和PDA.J2ME設備有:*有限的輸入能力(沒有鍵盤!) *小的顯示尺寸*有限的內存容量和堆大小*慢速的CPU在J2ME平臺上寫出快的遊戲——寫出在比桌面電腦裏的慢得多的CPU上運行的代碼更是挑戰了開發 者。

什麼時候不優化如果你不是在寫一個技能或者動作遊戲,那麼可能不需要優化。如果玩家已經爲自己的下一步考慮了幾秒鐘抑或幾分鐘,她可能不會介意如果你的遊 戲響應花掉了幾百微秒。這個規則的一個例外是,如果這個遊戲在決定下一步如何運行的時候有大量的工作要處理,比如搜索一百萬個可能的象棋片組合。這種情況 下,你可能想要優化你的代碼,從而在幾秒鐘內計算出電腦的下一步,而不是幾分鐘。

就算你正在寫這種類型的遊戲,優化也可能是危險的。許多這樣的技術伴隨着一個代價——他們表示着好“的程序設計這個通常概念飛過來的時候,同時使你的代碼 更難讀懂。有些是一個權衡,需要開發者大大增加程序的大小來得到性能上一點點的改進。J2ME開發者們對於保持他們的JAR儘可能的小這個挑戰再熟悉不過 了。這裏是一些不優化的理由:*優化是一個增加bug的好手*有些技術會降低你的代碼的移植性*你可能要花費大量的努力來得到微小的或者沒有改進*優化是 困難的最後一點需要一些闡述。優化是一個活動目標,在Java平臺上更是這樣,而且在J2ME上就更加突出,因爲其運行環境是那樣的多變。

你優化後的代碼可能在一個模擬器上運行得更快,但卻在實際設備上更慢,或者相反。爲一部手機優化可能會降低其在另一部上的性能

不過還是有希望。有兩條路徑你可以做優化,高層的和底層的。第一條基本上會在所有的平臺上增加執行性能,甚至會改進你代碼的整個質量。第二條是可能會讓你頭疼的,但是那些底層技術是很容易創造的,而且更加容易消去如果你不想使用它們。最起碼,他們看起來很有趣。

我們將用系統的timer在實際設備上剖析你的代碼,這可以幫助你測量出那些技術在你所開發的硬件上到底有多有效。

最後一點:*優化是有趣的

一個反面例子:讓我們來看一看這個包含兩個類的簡單的應用程序,首先,是Midlet……

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
public class OptimizeMe extends MIDlet implements CommandListener {
private static final boolean debug = false;
private Display display;
private OCanvas oCanvas;
private Form form;
private StringItem timeItem = new StringItem( "Time: ", "Unknown" );
private StringItem resultItem =
new StringItem( "Result: ", "No results" );
private Command cmdStart = new Command( "Start", Command.SCREEN, 1 );
private Command cmdExit = new Command( "Exit", Command.EXIT, 2 );
public boolean running = true;
public OptimizeMe() {
display = Display.getDisplay(this);
form = new Form( "Optimize" );
form.append( timeItem );
form.append( resultItem );
form.addCommand( cmdStart );
form.addCommand( cmdExit );
form.setCommandListener( this );
oCanvas = new OCanvas( this );
}
public void startApp() throws MIDletStateChangeException {
running = true;
display.setCurrent( form );
}
public void pauseApp() {
running = false;
}
public void exitCanvas(int status) {
debug( "exitCanvas - status = " + status );
switch (status) {
case OCanvas.USER_EXIT:
timeItem.setText( "Aborted" );
resultItem.setText( "Unknown" );
break;
case OCanvas.EXIT_DONE:
timeItem.setText( oCanvas.elapsed+"ms" );
resultItem.setText( String.valueOf( oCanvas.result ) );
break;
}
display.setCurrent( form );
}
public void destroyApp(boolean unconditional)
throws MIDletStateChangeException {
oCanvas = null;
display.setCurrent ( null );
display = null;
}
public void commandAction(Command c, Displayable d) {
if ( c == cmdExit ) {
oCanvas = null;
display.setCurrent ( null );
display = null;
notifyDestroyed();
}
else {
running = true;
display.setCurrent( oCanvas );
oCanvas.start();
}
}
public static final void debug( String s ) {
if (debug) System.out.println( s );
}
}
Second, the OCanvas class that does most of the work in this example...
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import java.util.Random;
public class OCanvas extends Canvas implements Runnable {
public static final int USER_EXIT = 1;
public static final int EXIT_DONE = 2;
public static final int LOOP_COUNT = 100;
public static final int DRAW_COUNT = 16;
public static final int NUMBER_COUNT = 64;
public static final int DIVISOR_COUNT = 8;
public static final int WAIT_TIME = 50;
public static final int COLOR_BG = 0x00FFFFFF;
public static final int COLOR_FG = 0x00000000;
public long elapsed = 0l;
public int exitStatus;
public int result;
private Thread animationThread;
private OptimizeMe midlet;
private boolean finished;
private long started;
private long frameStarted;
private long frameTime;
private int[] numbers;
private int loopCounter;
private Random random = new Random( System.currentTimeMillis() );
public OCanvas( OptimizeMe _o ) {
midlet = _o;
numbers = new int[ NUMBER_COUNT ];
for ( int i = 0 ; i < numbers.length ; i++ ) {
numbers[i] = i+1;
}
}
public synchronized void start() {
started = frameStarted = System.currentTimeMillis();
loopCounter = result = 0;
finished = false;
exitStatus = EXIT_DONE;
animationThread = new Thread( this );
animationThread.start();
}
public void run() {
Thread currentThread = Thread.currentThread();
try {
while ( animationThread == currentThread && midlet.running
&& !finished ) {
frameTime = System.currentTimeMillis() - frameStarted;
frameStarted = System.currentTimeMillis();
result += work( numbers );
repaint();
synchronized(this) {
wait( WAIT_TIME );
}
loopCounter++;
finished = ( loopCounter > LOOP_COUNT );
}
}
catch ( InterruptedException ie ) {
OptimizeMe.debug( "interrupted" );
}
elapsed = System.currentTimeMillis() - started;
midlet.exitCanvas( exitStatus );
}
public void paint(Graphics g) {
g.setColor( COLOR_BG );
g.fillRect( 0, 0, getWidth(), getHeight() );
g.setColor( COLOR_FG );
g.setFont( Font.getFont( Font.FACE_PROPORTIONAL,
Font.STYLE_BOLD | Font.STYLE_ITALIC, Font.SIZE_SMALL ) );
for ( int i = 0 ; i < DRAW_COUNT ; i ++ ) {
g.drawString( frameTime + " ms per frame",
getRandom( getWidth() ),
getRandom( getHeight() ),
Graphics.TOP | Graphics.HCENTER );
}
}
private int divisor;
private int r;
public synchronized int work( int[] n ) {
r = 0;
for ( int j = 0 ; j < DIVISOR_COUNT ; j++ ) {
for ( int i = 0 ; i < n.length ; i++ ) {
divisor = getDivisor(j);
r += workMore( n, i, divisor );
}
}
return r;
}
private int a;
public synchronized int getDivisor( int n ) {
if ( n == 0 ) return 1;
a = 1;
for ( int i = 0 ; i < n ; i++ ) {
a *= 2;
}
return a;
}
public synchronized int workMore( int[] n, int _i, int _d ) {
return n[_i] * n[_i] / _d + n[_i];
}
public void keyReleased(int keyCode) {
if ( System.currentTimeMillis() - started > 1000l ) {
exitStatus = USER_EXIT;
midlet.running = false;
}
}
private int getRandom( int bound )
{ // return a random, positive integer less than bound
return Math.abs( random.nextInt() % bound );
}
}



這個程序是一個模擬一個簡單遊戲循環的MIDlet:

*work 執行

*draw 繪製

*poll for user input 等待用戶輸入

*repeat 重複對於快速遊戲

這個循環一定要儘可能的緊湊和快速。我們的循環持續一個有限的次數(LOOP_COUNT=100),並且用系統timer來計算整個作業花費了多少毫 秒,我們就可以測量並改善它的性能。時間和執行的結果會顯示在一個簡單的窗口上。用Start命令來開啓測試。按任意鍵會提前退出循環,退出按鈕用來結束 程序。 在大多數遊戲裏面,主遊戲循環中的作業會更新整個遊戲狀態——移動所有的角色,檢測並處理衝突,更新分數,等等。在這個例子裏面,我們並沒有做什麼特別有 用的事。程序僅僅是在一個數組之間做一些算數運算,然後把這些結果加起來。 run()函數計算了每次循環所花費的時間。每一幀,OCanvas.paint()方法都在屏幕上的16個隨機的地方顯示這個數。一般的,你可以用這個 方法在你的遊戲裏面畫出你的圖像元素,我們的代碼在該過程中作了一些有用的摹寫。

不管這些代碼看起來有多麼的無意義,它給了我們足夠的機會去優化它的性能。

哪裏去優化 —— 90/10規則在苛求性能的遊戲裏面,有90%的時間是在執行其中%10的代碼。我們的優化努力就應該針對這10%的代碼。我們用一個Profier來定 位這 10%. 要運行J2ME無線開發包中的profier工具,選擇edit菜單下的preferences選項。 這將會顯示preferences窗口。選擇Monitoring這一欄,將"Enable Profiling"懸賞,然後點ok按鈕。什麼也沒有出現。這是對的,在Profier窗口顯示之前,我們需要在模擬器中運行我們的程序然後退出。現在 就做。

我的模擬器(運行在Windows XP下,Inter P4 2.4GHz的CPU)報告我100次這個循環用了6,407毫秒,或者說6又1/2秒。這個程序報告說62或者63毫秒每幀。在硬件(一個 motorola的i85s)上運行會慢得多。 一幀的時間大約是500毫秒,整個循環用了52460毫秒。

當你退出這個程序時,profiler窗口就會出現,然後你會看見一個文件夾瀏覽器中有一些東西,在左邊的面板上會有一個熟悉的樹形部件。方法間的聯繫會 在這個結構列表中顯示。每一個文件夾是一個方法,打開一個文件夾會顯示它所調用過的方法。在該樹中選擇一個方法會顯示那個方法的profiling信息並 在右邊的面板顯示所有被它調用過的方法。注意在每一個元素旁邊顯示了一個百分數。這就是該方法在整個執行過程中所佔的執行時間的百分比。我們必須翻遍這棵 樹,來尋找時間都到哪裏去了,並對佔用百分比最高的方法進行優化,如果可能的話。

對這個profiler,有幾點需要說明。首先你的百分比多半會和我的不一樣,但是他們的比例會比較相似——總是在最大的數之後。我的數據在被次運行的時 候都會改變。爲了保持情況一致,你可能希望關掉所有的後臺程序,像Email客戶端,並在你測試的時候保持你正在進行的任務最少。還有,不要在用 profiler之前混淆(obfuscate)你的代碼,不然你的方法會被神祕的標示爲b或者a或者ff.最後profiler不會因爲你運行模擬器的 設備的差別而改變,它和硬件是完全獨立的。

打開最高百分比的那個文件夾,我們看到有66.8%的時間在執行一個被稱爲 "com.sun.kvem.midp.lcdui.EmulEventHandler$EventLoop.run"的方法,這個對我們並沒有什麼幫 助。用類似的方法,再往下尋找更深層次的方法,持續下去,你就會找到一個大的百分比停留在serviceRepaints()上,最後到了我們的 OCanvas.paint()方法。另外有30%的時間在OCanvas.run()方法裏。這兩個方法都在我們的主程序循環中,這並不奇怪。我們不會 在我們的MIDlet類中花任何時間做優化,同樣地我們不會對遊戲的主循環外的任何代碼做優化。

在我們的例子程序中的百分比的劃分在真實的遊戲中並不是完全的沒有特性。 你多半會在一個真實的視覺遊戲中發現這個大的執行時間的比例是在paint()方法中。 相比於非圖形化程序,圖形化程序總是要花很長的時間。 不幸的是,我們的圖形程序已經被寫在了J2ME API這一層下,對於改善它們的性能,我們沒有多少可以做的。我們可以做的是在用哪個和如何用它們之間做出聰明的決定。

 
發佈了6 篇原創文章 · 獲贊 1 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章