这是一篇翻译自 Mike Shivas 的文章,如果对原文感兴趣,可以访问英文原著。
如果您对本译文有任何疑问或者意见,请直接对本 blog 文章发表评论,或者给我发送电子邮件。
本文描述了优化在为移动设备编写游戏中所扮演的角色。笔者将通过范例说明,如何、何时以及为何要通过优化编码来“压榨” MIDP 相关手持设备上的最后“一滴”性能。我们将讨论为什么优化是必要的以及什么时候通常最好不要优化。笔者将解释高端和低端优化之间的区别,同时学会如何使用同 J2ME Wireless Toolkit 一起发布的 Profiler 工具。最后这篇文章将揭示很多方便您移植 MIDlets 的技术。
为什么要优化?
我们大致可以把视频游戏宽泛地分成两类:实时游戏和输入驱动游戏。输入驱动游戏显示游戏当前的状态,在继续工作之前,会无限制地等待,直到有用户输入发生。棋牌类游戏就属于此类,还有大部分迷题类、策略类以及文字型冒险游戏。实时游戏,有时候称为技巧或动作游戏,并不会等待游戏者,他们会一直工作,直到 Game Over。
技巧或动作游戏的特性,通常体现在屏幕上大量地移动(想象一下 Galaga [注]或者 Robotron [注])。刷新频率必须至少有 10 fps (frame per second,每秒帧数),并且要有足够的动作来保持游戏有挑战性。这些游戏需要游戏者有快速的反应能力以及良好的眼手协调能力,因此成功的 S&A 游戏同样要求对用户的输入有非常好的回馈。提供在高刷新率的图形运算的同时对按键进行快速的响应,正是为什么实时游戏需要高效率编码的原因。在使用 J2ME 进行开发的时候,这更是一项极大的挑战。
Java 2 Micro Edition (J2ME) 是一个被修剪过的 Java 版本,适合能力非常有限的小型设备,比如手机和 PDA。J2ME 设备有如下特征:
- 受限的输入能力(没有键盘!)
- 显示区域小
- 有限的存储空间和堆
- 较慢的 CPU
用 J2ME 平台来开发游戏,对于开发者来讲是一个挑战,因为他们编写的代码需要运行在远远慢于台式机的 CPU 之上。
什么时候不优化?
如果你不是在编写一个技巧或者动作游戏,那么可能没有必要进行优化。如果游戏者每几秒钟或几分钟才进行下一步操作,那么他可能并不会介意你的游戏在响应他的动作时多花了几百毫秒。这条准则的一个例外,是当游戏在决定下一步操作时,需要进行大量的运算,比如在上百万种可能的棋谱中进行搜索,那么你可能需要对代码进行优化,以确保下一步能在几秒钟内计算出来,而不是几分钟。
如果你在编写这类的游戏,优化的过程可谓艰辛。很多这样的技术通常伴随着其它的代价——它们不再是传统意义上的“好”程序,它们使得你的代码不易阅读。我们需要做出权衡,因为有的时候为了取得那么一点点性能上的改善,却需要我们显著地增大一个程序。J2ME 的开发者都太熟悉要尽可能地让 JAR 包变小。以下是更多不进行优化的理由:
- 优化很容易引入 bug
- 有些技术会降低代码的移植性
- 你花费了很多努力却只有很小甚至没有结果
- 优化真的很难做
最后一点需要澄清的是,优化是一个变化的目标,在 Java 平台上是这样,在 J2ME 平台上更是如此,因为运行环境千差万别。你优化过的代码在某个模拟器上可能更快,但是在真实设备上却更慢,反之亦然。在某一种手机上的优化可能实际上降低了在另一种上的性能。
但是希望还是有的。在优化的时候,我们有两种程度,高端的和低端的。前者类似于在所有的平台上提高运行性能,提高代码的整体质量;而后者可能更让你头痛,不过这些低端技术比较容易引入,并且在你不需要他们时也更容易忽略。至少,它们看起来非常有趣。
我们同样用系统时钟来描述在实际设备上的代码,这将帮助你估算在要部署的目标硬件上,这些技术到底多有效率。
最后,重要的一点——
优化充满了乐趣!
一个反面范例
现在让我们来看一个简单的例子,它有两个类,首先是 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 ); } }
然后,OCanvas 做了本例中大部分的工作——
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,模拟一个简单的游戏循环:
- 计算
- 画图
- 处理输入
- 反复
对于高速的游戏,这个循环应该尽可能的紧凑。我们的循环进行有限的次数 (LOOP_COUNT = 100),并且用一个系统时钟来计算整个过程花费了多少时间(毫秒),这样我们就能测量并且改进其性能。时间和结果显示在一个简单的表单中。用“开始”命令来启动测试,按任意键中止循环,用“退出”命令退出程序。
在大多数游戏中,游戏主循环的“计算”部分包含了对游戏世界状态的更新——移动主角,测试和响应碰撞,更改分数等。在这个例子中,我们并没有做任何有意义的操作,而只是简单的运行一个数组,对其中每个数字进行一些数学计算,并给出一个总的计算结果。
run() 方法同样计算出每次循环所花的执行时间。每一帧,OCanvas.paint() 方法都将在屏幕上 16 个位置的其中随机之一显示这个时间的毫秒数。正常情况下你可能会在这个方法中绘制游戏中的元素,这里我们的代码只是对这个过程提供一个合理的模拟。
不管这段代码看起来多么没有意义,它都将给我们提供很大的空间来改善运行性能。
(四) (五)

暂无评论