一个反面例子:
让我们来看一看这个包含两个类的简单的应用程序,首先,是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+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来计
类的最前面.
public static final Font font =
Font.getFont( Font.FACE_PROPORTIONAL,
Font.STYLE_BOLD | Font.STYLE_ITALIC,
Font.SIZE_SMALL);
public static final int graphicAnchor =
Graphics.VCENTER | Graphics.HCENTER;
public static final int textAnchor =
Graphics.TOP | Graphics.LEFT;
private static final String MESSAGE = " ms per frame";
private String msMessage = "000" + MESSAGE;
private Image stringImage;
private Graphics imageGraphics;
private long oldFrameTime;
来回顾一下那个方法:
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;
}
每次在run()中的循环,我们都传递一个数组参数。在work()中的循环外计算了我们的除数,然后调用workMore()来做这个除法。这
那么除数是不变的,真的属于内循环外面。
但是让我们多想一想,这个调用本身就是完全不必要的。下面的代码做了同样的事情...
public synchronized int work( int[] n ) {
r = 0;
divisor = 1;
for ( int j = 0 ; j < DIVISOR_COUNT ; j++ ) {
for ( int i = 0 ; i < n.length ; i++ ) {
r += workMore( n, i, divisor );
}
divisor *= 2;
}
return r;
}
...没有对getDivisor()的调用。现在我们的profiler告诉我们run()方法花了23.72%的时间,对应于我们做这些改进以前的38.78%
越大。
public final static int work( int[] n ) {
divisor = 1;
r = 0;
for ( int j = 0 ; j < DIVISOR_COUNT ; j++ ) {
for ( int i = 0 ; i < n.length ; i++ ) {
r += n * n / divisor + n;
}
divisor *= 2;
}
return r;
}
哇哦!去掉workMore()的调用把run中的时间开销砍到了9.96%。现在开始,一路上都会上升。让我们来看一看两个最基本的优化技术
COUNT只是8,解开我们的循环变得简单起来。
public final static int work( int[] n ) {
r = 0;
for ( int i = 0 ; i < n.length ; i++ ) { r += n * n + n; }
for ( int i = 0 ; i < n.length ; i++ ) { r += (n * n >> 1) + n; }
for ( int i = 0 ; i < n.length ; i++ ) { r += (n * n >> 2) + n; }
for ( int i = 0 ; i < n.length ; i++ ) { r += (n * n >> 3) + n; }
for ( int i = 0 ; i < n.length ; i++ ) { r += (n * n >> 4) + n; }
for ( int i = 0 ; i < n.length ; i++ ) { r += (n * n >> 5) + n; }
for ( int i = 0 ; i < n.length ; i++ ) { r += (n * n >> 6) + n; }
for ( int i = 0 ; i < n.length ; i++ ) { r += (n * n >> 7) + n; }
return r;
}
有两个重点,第一,你会发现解开我们的循环需要我们复制一些代码。这是你在J2ME中想要的最后一件事,程序员们总是在与JAR作战
安排他们,像这样...
private static int divisor;
private static int r;
private static int ni;
public final static int work( int[] n ) {
r = 0;
for ( int i = 0 ; i < n.length ; i++ ) {
ni = n;
r += ni * ni + ni;
r += (ni * ni >> 1) + ni;
r += (ni * ni >> 2) + ni;
r += (ni * ni >> 3) + ni;
r += (ni * ni >> 4) + ni;
r += (ni * ni >> 5) + ni;
r += (ni * ni >> 6) + ni;
r += (ni * ni >> 7) + ni;
}
return r;
}
...把run()中耗费的时间减少到了6.18%,一点也不坏。在我们继续之前,让我多说一点关于数组的事。一个稍微高级的优化(也就是
顺序并像这样重写方法,我们就可以和零比较:
public final static int work( int[] n ) {
int r = 0;
int ni;
int nis;
int i;
for ( i = n.length ; --i >= 0 ; ) {
ni = n;
nis = ni * ni;
r += nis + ni;
r += (nis >> 1) + ni;
r += (nis >> 2) + ni;
r += (nis >> 3) + ni;
r += (nis >> 4) + ni;
r += (nis >> 5) + ni;
r += (nis >> 6) + ni;
r += (nis >> 7) + ni;
}
return r;
}
就是它了!这个代码可能会快一点,但是 profiler的结果不是那么的明显,清楚地是这个方法变得难懂了。或许这里还有更多可改进
其他的技术
一个我不能在我的示例程序中包含的技术是,最佳的使用switch()。Switch非常普遍的用于实现有限状态自动机(Finite State
Machines),在为非玩家角色的行为控制做人工智能的代码时。在你使用switch的时候,像这样写代码是一个好的编程习惯:
public static final int STATE_RUNNING = 1000;
public static final int STATE_JUMPING = 2000;
public static final int STATE_SHOOTING = 3000;
switch ( n ) {
case STATE_RUNNING:
doRun();
case STATE_JUMPING:
doJump();
case STATE_SHOOTING:
doShoot();
}
这没有什么不对的,这些变量很不错而且离得很远,万一我们想要加一个在RUNNING和JUMPING之间的变量,像 STATE_DUCKING =
好:
public static final int STATE_RUNNING = 1;
public static final int STATE_JUMPING = 2;
public static final int STATE_SHOOTING = 3;
在使用定点数学库(Fixed Point math library)的时候,有一些优化你可以做。首先,如果你除了一个相同的数很多次,你应该计
算出那个数的倒数然后把运算改变为执行一个乘法。乘法要比除法快一点。所以不是...
int fpP = FP.Div( fpX, fpD );
int fpQ = FP.Div( fpY, fpD );
int fpR = FP.Div( fpZ, fpD );
...你应该把它重新写成这样:
int fpID = FP.Div( 1, fpD );
int fpP = FP.Mul( fpX, fpID );
int fpQ = FP.Mul( fpY, fpID );
int fpR = FP.Mul( fpZ, fpID );
如果你在每一帧要做数百次的除法,这会有所帮助。第二点,不要默认你的FP数学函数库不错。don't take your FP math library
for granted. 如果你有它的源代码,打开它然后看一下里面发生了什么。保证所有的方法都被声明为final static并看看有没有机
会优化它的代码。比如,你可能发现这个乘法方法需要把int强制转换为long然后再转换回来:
public static final int Mul (int x, int y) {
long z = (long) x * (long) y;
return ((int) (z >> 16));
}
那些转换要花时间。冲突检测使用边界圆球或者半球(bounding circles or spheres)包括将int的平方相加。那会产生一些大的
可能会溢出你的int数据类型的最大值的数字。要避免这个,你可以写下自己的返回一个long型数的平方函数:
public static final long Sqr (int x) {
long z = (long) x;
z *= z;
return (z >> 16);
}
这个优化的方法避免了两个转换。如果你要做大量的定点计算,你可能要考虑把所有的主游戏循环中的调用替换为long型的。那会节
次来绘制一个五倍大小的图片慢得多。这个只是肯定会帮助我设计我的下一个游戏。
祝你好运,玩得开心!
资源:
1. J2ME's official web site contains the latest on what's happening on this front.
2. Like wireless games? Read the Wireless Gaming Review.
3. Discuss J2ME Game Development at j2me.org
4. A great site on many aspects of Java Optimization
5. Another great site on Optimization
6. Many articles on J2ME performance tuning
7. The amazing Graphics Programming Black Book by Michael Abrash
8. The Art of Computer Game Design by Chris Crawford