J2ME 游戏优化探密(一)

这是一篇翻译自 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 个位置的其中随机之一显示这个时间的毫秒数。正常情况下你可能会在这个方法中绘制游戏中的元素,这里我们的代码只是对这个过程提供一个合理的模拟。

不管这段代码看起来多么没有意义,它都将给我们提供很大的空间来改善运行性能。

(一)

(二)

(三)

(四) (五)

发表评论

Login with Sina Weibo account

或者,也可以直接用下面的表单。 电子邮件地址不会被公开。

*

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

暂无评论