本文将讲述代码优化对于编写高效的手机游戏的重要性。我将使用例子来为大家讲述何时使用何种方法来优化你的代码,使得我们的程序尽可能的挖掘MIDP在手机中所剩无几的表现力。我们将会看到如何使用J2ME Wireless Toolkit的Profiler功能来找出优化代码的位置,最后我会给出编写J2ME程序的忠告。

  为什么要优化?

  游戏能够被划分为两种较广泛的类型:实时型和按键驱动型。按键驱动类型的游戏一般在屏幕上显示游戏目前所处的状态,并且等待玩家输入某些按键来驱动游戏继续运行。棋牌类游戏,大部分的解谜类游戏以及策略类和文字类游戏都属于这类游戏。而实时游戏,特别是动作游戏不会等待玩家的输入,他们一直运行直到游戏的结束。

  动作类游戏一个重要的特征就是将大量的运算花在屏幕显示上,而且刷新率(FPS)必须保持在10以上。这类游戏还必须要有大量的动作来使得游戏具有更高的挑战性。这类游戏需要玩家有快速的反应和很好的手眼协调能力,所以这类游戏对键盘输入的反应也有极高的要求。为使得程序在高速运行时能对键盘做出及时的反应,图像能以高速的FPS来运行,我们就必须优化我们的代码,以使得我们的程序能以最快的速度运行。

  J2ME是一个精简的Java版本,适合只有有限容量的小型设备,例如手机和PDA,J2ME设备有:

  ·有限的输入能力

  ·屏幕尺寸很小

  ·受限的内存及堆大小

  ·很慢的CPU

  这些特点使得在J2ME平台上编写快速的游戏并不是件易事。相对于电脑游戏来说,编写J2ME平台上的游戏会对程序有更高的要求和更进一步的挑战。

  何时不用优化?

  如果你编写的游戏不时动作游戏,有可能不必去优化。假如玩家需要几秒钟甚至几分钟去思考他下一步怎样走,那他将不会注意到你的游戏的按键响应超过几百毫秒。不过一个例外是如果你的程序需要大量的运算去决定它下一步怎样走,例如棋类游戏需要在几百万种组合中去寻找。假如这样的话,你有可能想要优化你的代码以使得电脑仅需要几秒钟而不是几分钟去决定他下一步的动向。

  即使你正在编写这类游戏,优化也是相当危险的。这些技术将伴随着高昂的代价,他将使你的代码不易读。这就需要开发者自己去平衡。增大了JAR文件的大小来换取程序少量性能的提高是否是值得的。下面给出不要去优化的更多的理由。

  ·优化将很容易引入bug

  ·有些优化技术将使得移植变得困难

  ·有可能你付出了巨大的努力却收效甚微

  ·统一的优化一般比较困难

  我需要对最后一点做一些解释:由于优化的是一个不定的目标及平台,有些方法可能在Java平台上运行快一点,而有些可能在J2ME平台上运行快一点。由于执行环境存在着巨大的差异,你的代码可能在模拟器上跑得很快,但是在真机上却很慢。反之也有可能。你对一种机型的优化可能会造成在另一种平台上性能的下降。

  但这些并不代表我们没有了希望。你可以通过两种方法来进行优化,高级优化和低级优化。高级优化可以在任意的平台上来提高程序的性能,甚至全面的提高代码的质量。而低级优化则是相对于某种特定机型的优化技术。高级优化即算法上的优化,低级优化即程序层面对于单个函数执行效率的优化。Michael Abrash, Quake的开发者之一,曾经写到:"the best optimizer is between your ears". (最好的优化者在你的两耳之间).使用优良的算法所能提高的效率比在一个普通算法的程序上使用低级优化所能提高的效率要高得多。

  我们将使用J2ME Wireless Toolkit的Profiler去剖析你的代码的运行效率,它也将帮助你更精确的测量出这些技术对于提高性能的有效性。

  在哪里优化?

  在动作类游戏中,90%的程序执行时间被花费在了10%的代码段中。这10%的代码段是我们需要集中全力去优化的部分。我们将使用profiler去找到这10%代码的位置。照下图我们打开J2ME Wireless Toolkit的Profiler功能。

  我们必须要在模拟器中运行程序并退出后才会弹出Profiler窗口,如下图:




注意在左窗口中该函数所占用cpu百分比,这个百分比是总执行时间花在每个特定函数上的百分比,接下来我们只要找到占用百分比最多的那个函数来优化就可以了。

  有几点需要注意的。首先:你的百分比数和我的肯定有很大的不同,但是有一点是相同的即花费时间最长的函数所占的百分比数最大。每次运行程序我的百分比数都会不同,为了尽可能的保持统一,我们在测试前应当关闭其他的程序。其次:测试时不要混淆,否则你将看到函数名类似于a,b,z..最后:Profiler并不知道你模拟的是什么机型。记住,真机才是真理。
如何优化?

  知道了在那里优化,但如何进行优化呢?我们知道大部分的运算时间被花在了绘图函数上,J2ME已经为我们提供了这些函数,我们没有办法对这些函数的内部进行优化,但是我们仍然有选择权。下面我们来对J2ME提供给我们的绘图函数作一测试。

  在Canvas类里添加测试代码如下:

protected void paint(Graphics g) {
 TestPaint(g);
}
void TestPaint(Graphics g) {
 setClip(g);
 setColor(g);
 m_font = getFont();
 setFont(g);
 drawString(g);
 drawRect(g);
 fillRect(g);
 drawImage(g);
 drawRegion(g);
 drawArc(g);
 drawChar(g);
 drawLine(g);
 drawRoundRect(g);
 fillArc(g);
 fillRoundRect(g);
}

void setColor(Graphics g) {
 g.setColor(0);
}

void drawArc(Graphics g) {
 g.drawArc(0, 0, 100, 100, 4, 4);
}

void drawChar(Graphics g) {
 g.drawChar('你', 0, 0, GE.TOPLEFT);
}

void drawRoundRect(Graphics g) {
 g.drawRoundRect(0, 0, 100, 100, 4, 4);
}

void fillRoundRect(Graphics g) {
 g.fillRoundRect(0, 0, 100, 100, 4, 4);
}

void fillArc(Graphics g) {
 g.fillArc(0, 0, 100, 100, 4, 4);
}

void drawLine(Graphics g) {
 g.drawLine(0, 0, 100, 100);
}

Font getFont() {
 return Font.getFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_SMALL);
}

void setFont(Graphics g) {
 g.setFont(m_font);
}

void drawString(Graphics g) {
 g.drawString("你好", 0, 0, GE.TOPLEFT);
}

void drawRect(Graphics g) {
 g.drawRect(0, 0, 100, 100);
}

void setClip(Graphics g) {
 g.setClip(0, 0, 300, 300);
}

void fillRect(Graphics g) {
 g.fillRect(100, 0, 100, 100);
}

void drawImage(Graphics g) {
 g.drawImage(GE.m_images[GRes.PNG_MAP], 0, 100, GE.TOPLEFT);
}

void drawRegion(Graphics g) {
 g.drawRegion(GE.m_images[GRes.PNG_MAP], 0, 0, 100, 100, Sprite.TRANS_MIRROR,100, 100, GE.TOPLEFT);
}

  该程序各函数分别绘制100*100的图形,经过一段时间以后,退出应用程序,我们得到如下图数据:

  根据Profiler窗口所显示的数据,我们发现drawString最耗时。其次是drawRegion,所以我们应尽量避免使用drawString函数。

  通过Profiler对各种函数及程序的测试,我总结如下结论:

  ·仅当你需要的时候才去优化代码!

  ·仅优化那些最耗时的代码!

  ·使用Profiler去查找哪里需要优化!
 
  ·记住Profiler不代表真机上的优化结果,使用System Timer来在真机上做最后的测试!

  ·在做低级优化之前,总是要先思考算法是否是最优!

  ·绘图是很占用时间的,所以尽可能的减少Graphics函数的调用!

  ·尽可能的使用SetClip()来减少绘图区域,相对于SetClip(),drawImage()所花的时间会更可观!

  ·尽可能的将变量定义在循环以外!

  ·尽最大可能的进行对需要的数据进行预先计算并将结果保存在缓冲里!

  ·String类很容易产生垃圾内存,尽可能的使用StringBuffer代替String或用final static来定义之!

  ·假设是不被接受的,一切要以真机为据!

  ·尽量使用static final修饰函数,而避免synchronized修饰符!

  ·对于频繁调用的函数要使用尽可能少的参数!

  ·尽可能的不使用函数调用!

  ·尽可能的使用<<和>>来代替*和/!

  ·使用位操作来代替%运算!

  ·与0比较比与其他数值比较快!

  ·数组存取比C语言慢,尽可能不在循环中存取数组!

  ·局部变量比其他类型的变量运算要快!

  ·在switch()中尽量使用连续的小数值判断!

  ·尽量使用乘法而不使用除法!
  
  ·尽量使用已有算法!