程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> J2ME >> J2ME游戲優化秘密

J2ME游戲優化秘密

編輯: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窗口顯示之前,我們需要

  在模擬器中運行我們的程序然後退出。現在就做.圖1顯示了如何打開Profiler工具。

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

  在本文這一課中,我們將試著改善這個數據。

  當你退出這個程序時,profiler窗口就會出現,然後你會看見一個文件夾浏覽器中有一些東西,在左邊的面板上會有一個熟悉的樹形部件。方法間的聯系會在這個結構列表中顯示。每一個文件夾是一個方法,打開一個文件夾會顯示它所調用過的方法。在該樹中選擇一

  個方法會顯示那個方法的profiling信息並在右邊的面板顯示所有被它調用過的方法。注意在每一個元素旁邊顯示了一個百分數。這就是該方法在整個執行過程中所占的執行時間的百分比。我們必須翻遍這棵樹,來尋找時間都到哪裡去了,並對占用百分比最高的方法進行優化,如果可能的話。

  圖2 -- Profiler程序調用的圖

  對這個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這一層下,對於改善它們的性能,我們沒有多少可以做的.我們可以做的是在用哪個和如何用它們之間做出聰明的決定.

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved