Showing posts with label J2ME. Show all posts
Showing posts with label J2ME. Show all posts

Tuesday, March 02, 2010

NOKIA 手机安装 MIDlet 时出现“Conflicting application”错误及解决

最近发现在某些诺基亚(NOKIA)手机上安装带有 Push Registry 功能的 MIDlet 时,会发生安装失败的情况。错误信息有可能是“Push registry failure”(S60v3),或者“Conflicting application”(S40v6),出现这个错误的直接原因,应该是安装程序在执行 Push Registry 注册时失败,认为要注册的目标端口已经被其它应用程序占用。

实际的情况是之前安装过同样的 MIDlet 的旧版本并已经删除。然而,无论是软格机、硬格机,用 Nokia Software Updater 重装/升级 Firmware,都无法解决。

最后发现一个简单但有效的解决办法:
  • 将 JAD Property 的 MIDlet-Push-1 中 Push Registry 的端口改成别的端口
  • 这时 MIDlet 应该能够正常安装。如果你的应用并不在乎使用哪个具体的端口,那么到这里问题就已经解决了
  • 如果应用必须使用之前的那个固定端口,那么将安装好的 MIDlet 删除(卸载)掉,将 JAD 中的端口再改回去,然后就可以正常安装了

Friday, April 17, 2009

SAMSUNG 手机安装 MIDlet 出现“内容不匹配”

昨天通过 OTA 方式从 Apache 服务器上在一部 SAMSUNG SGH-L760 上安装 MIDlet,下载完 JAD 确认安装,开始下载 JAR 之前,手机报错说“内容不匹配 (Content mismatch)”。同样的 JAD 和 JAR 在另外一个主机上就能成功下载安装,而且这一组 JAD 和 JAR 在同服务器上另外一个 HTTP 服务应用上也能成功安装,一时觉得纳闷。

开始怀疑是端口问题,因为出现安装错误的 Apache 服务器是运行在 82 端口上的。但是同样是从这个 Apache 服务,另外一部 SonyEricsson K610i 就能正常下载安装,而且如果是端口问题,没道理 JAD 能下载而 JAR 不能。

然后开始怀疑是 JAR 文件的 Content-Type 问题,因为在确认安装的界面上,有显示应用类型是 application/vnd.sun.j2me.java-archive。打开 Apache 的配置文件 /etc/httpd/conf/httpd.conf,发现没有定义 .jad.jar 文件的语句,于是添加如下两行:
AddType text/vnd.sun.j2me.app-descriptor .jad
AddType application/vnd.sun.j2me.java-archive .jar
然后重启 Apache 服务,问题解决。MIDlet 成功在 SAMSUNG SGH-L760 上安装了。

Friday, October 10, 2008

MIDlet 在索爱手机上安装失败

最近在工作中,发现一部分索爱 (SonyEricsson) 手机,在安装 MIDlet 时发生“Downloading failed”错误。具体情况发生在手机通过 OTA 安装 MIDlet,Jar 文件下载完毕并开始安装后,系统询问是否允许应用程序自动启动,以及是否允许应用程序收发短信,接下来就出现“Downloading failed”。

在出现此问题时,使用的是 Orange France 的 SIM 卡,考虑到 GPRS 漫游可能会带来的问题,我们改用中国移动的 SIM 卡,则安装正常。看来和 SIM 卡有关系。但同时,在索爱手机上无法安装的 Orange France 的 SIM 卡,在其它一些索爱手机以及所有 NOKIA 手机上都能正确安装,所以并不是由 SIM 卡本身 GPRS 漫游引起的。

几个星期以来,一直被这个问题困扰,百思不得其解。今天修改一个关于 PushRegistry 的 bug,突然想起来可能和 PushRegistry 有关,又重新 Google 了一番,发现了索爱官方论坛上的这个帖子:

https://developer.sonyericsson.com/thread/17306

帖子的作者遇到和我们完全一致的问题,而最终的解决办法,由索爱的技术人员给出了答案。

这个无法安装的错误,确实和 PushRegistry 有关系。如果把 JAD 中“MIDlet-Push-*”的属性去掉,则可以正常安装。但导致出问题的最终原因,是手机短信选项设置中存在没有值得空选项,如“Service center”、“Message type”、“Validity period”。SIM 卡通常都会为这些选项自动设置,但如果某一个选项没有设置,成为空值,则会导致上述情况。

我仔细查看了我们有问题的手机,所有手机上的“Validity period”都为空。设置成“Network maximum”后,MIDlet 安装和 PushRegistry 注册就完全正常了。

另外,我们发现有问题的手机包括 K700、K500、W800、S700、K600 等,多为 JP-3、JP-5 平台的手机。

Tuesday, September 23, 2008

MIDlet 在 NOKIA 手机上安装失败

这两天发现一个奇怪的问题,就是 MIDlet 在 NOKIA 6680 上无法安装。在 Jar 下载完成后,会显示“Unable to install. Not enough memory!”而安装之前手机刚刚执行过出厂复位操作,机身空余存储空间约 7MB,因此,安装失败不可能是装不下 MIDlet 的内容而导致的。

经过反复研究,终于发现问题出在 Jad 文件属性 MIDlet-Data-Size 上。这个属性是 MIDlet 用来告诉手机,该应用程序将需要多少 RMS 存储空间。由于 NOKIA 6680 的 RMS 容量,实测有 7128 KB,和机身的空余存储空间相等,也就是说理论上手机能装多少,MIDlet 的 RMS 就能装多少,因此应用程序的 Jad 里头,MIDlet-Data-Size 设置的值是 133072,也就是 128KB。

经过反复修改 Jad 文件里 MIDlet-Data-Size 的值,发现允许的最大值为 65536,也就是 64KB。任何超过这个值的应用程序,在安装时手机都会拒绝。

同样的问题在 NOKIA 6131 上也同样存在。如果有其它手机也有类似问题,以后会随时更新。

Tuesday, March 14, 2006

J2ME 游戏优化探密(三)

循环之外?

for() 循环内的代码,循环多少次,它们就被执行多少次。因此,为了改进性能,我们需要尽可能的把代码放在循环之外。我们在描述器中会发现我们的 paint() 方法被调用了 101 次,并且循环执行了 16 次。我们可以把哪些东西放在循环之外呢?让我们从所有的声明(declaration)开始。每次调用 paint() 方法,我们都声明了一个 Font,一个 String,一个 Image 以及一个 Graphics 对象。我们可以将这些从该方法中挪出来,放到类的顶部。
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;

你会发现我把 Font 对象设置成为共有的常量。这在程序中通常很有用,比如这里你可以把常用到的字体在同一个地方集中定义。我发现锚(anchor)也是一样,因此对文字以及图形的锚也做了同样的处理。预先计算这些东西,保证了计算的结果,虽然微不足道,但是我们将它们从循环中挪了出来。

我同样把 MESSAGE 也设置成了常量。这是因为 Java 总是喜欢到处创建 String 对象。如果不控制好,这些字符串将消耗大量的内存。别不以为然,当你过多的消耗内存,将转而影响整体性能,尤其当垃圾收集器被频繁调用的时候。字符串制造垃圾,而垃圾是糟糕的;而使用字符串常量能缓解这个问题。稍后我们会讲到如何利用 StringBuffer 来完全解决由字符串泛滥带来的内存损耗。

现在我们将这些东西变成了实例变量(instance variables),我们需要在构造方法上添加如下代码:
stringImage = Image.createImage( font.stringWidth( msMessage ),
font.getBaselinePosition() );
imageGraphics = stringImage.getGraphics();
imageGraphics.setFont( font );

Graphics 对象提出来,另一点很爽的地方在于,我们只需要对字体设置一次,然后就可以完全不再去管它;而不用每次循环的时候都来设置它。不过仍然每次都需要调用 fillRect() 方法来擦去图像上的内容。编码狂人们可能会想要这样做,从同一个 Image 创建两个 Graphics 对象,然后将其中一个的颜色预设为 COLOR_BG 用来调用 fillRect(),另一个设置为 COLOR_FG 用来调用 drawString()。很遗憾,J2ME 中,对同一个 Image 多次调用 getGraphics() 方法的行为,定义的并不好,不同的平台会有不同的结果,因此你优化的结果可能对 Motorola 有效但 NOKIA 却不行。当无法确定的时候,不要做任何无根据假设。

还有另一个改进 paint() 方法的途径。动动我们的脑子我们会意识到,仅当 frameTime 的值发生改变的时候我们才需要重绘该字符串。这也就是我们引入新的变量 oldFrameTime 的原因。以下是新的方法:
public void paint(Graphics g) {
g.setColor( COLOR_BG );
g.fillRect( 0, 0, getWidth(), getHeight() );
if ( frameTime != oldFrameTime ) {
msMessage = frameTime + MESSAGE;
imageGraphics.setColor( COLOR_BG );
imageGraphics.fillRect( 0, 0, stringImage.getWidth(),
stringImage.getHeight() );
imageGraphics.setColor( COLOR_FG );
imageGraphics.drawString( msMessage, 0, 0, textAnchor );
}
for ( int i = 0 ; i < DRAW_COUNT ; i ++ ) {
g.drawImage( stringImage, getRandom( getWidth() ),
getRandom( getHeight() ), graphicAnchor );
}
oldFrameTime = frameTime;
}

描述器现在显示 OCanvaspaint() 方法所花费的时间占总时间的百分率已经下降到了 41.02%。相比以前由 drawString()fillRect() 引发的 101 次 paint() 来讲,现在只调用了 69 次。这是相当不错的改进,而且可以继续的空间不多了,这也是我们该认真起来的时候。优化总是越来越难。现在我们要刮掉循环中最后一点冗余的代码。我们可能只能再削减非常少的百分率,但是如果幸运的话,还是可以得到一些显著的改进。

让我们先从简单的开始。相比起调用 getHeight()getWidth() 方法,我们可以只调用它们一次,然后把结果缓存在循环之外。然后我们要停止使用 String,而用 StringBuffer 来手动地处理。我们可以通过调用 Graphics.setClip() 限制绘制区域来减少 drawImage() 调用所花费地时间。最后,我们要避免在循环中调用 java.util.Random.nextInt()

以下是新的变量……
private static final String MESSAGE = "ms per frame:";
private int iw, ih, dw, dh;
private StringBuffer stringBuffer;
private int messageLength;
private int stringLength;
private char[] stringChars;
private static final int RANDOMCOUNT = 256;
private int[] randomNumbersX = new int[RANDOMCOUNT];
private int[] randomNumbersY = new int[RANDOMCOUNT];
private int ri;

以下是新的构造方法……
iw = stringImage.getWidth();
ih = stringImage.getHeight();
dw = getWidth();
dh = getHeight();
for ( int i = 0 ; i < RANDOMCOUNT ; i++ ) {
randomNumbersX[i] = getRandom( dw );
randomNumbersY[i] = getRandom( dh );
}
ri = 0;
stringBuffer = new StringBuffer( MESSAGE+"000" );
messageLength = MESSAGE.length();
stringLength = stringBuffer.length();
stringChars = new char[stringLength];
stringBuffer.getChars( 0, stringLength, stringChars, 0 );

你会发现我们预先计算了 Display 和图像的区域,还缓存了对 getRandom() 方法的 512 次调用,并用 StringBuffer 取代了 msMessage 字符串。当然,最大的目标还是 paint() 方法:
public void paint(Graphics g) {
g.setColor( COLOR_BG );
g.fillRect( 0, 0, dw, dh );
if ( frameTime != oldFrameTime ) {
stringBuffer.delete( messageLength, stringLength );
stringBuffer.append( (int)frameTime );
stringLength = stringBuffer.length();
stringBuffer.getChars( messageLength,
stringLength,
stringChars,
messageLength );
iw = font.charsWidth( stringChars, 0, stringLength );
imageGraphics.setColor( COLOR_BG );
imageGraphics.fillRect( 0, 0, iw, ih );
imageGraphics.setColor( COLOR_FG );
imageGraphics.drawChars( stringChars, 0,
stringLength, 0, 0, textAnchor );
}
for ( int i = 0 ; i < DRAW_COUNT ; i ++ ) {
g.setClip( randomNumbersX[ri], randomNumbersY[ri], iw, ih );
g.drawImage( stringImage, randomNumbersX[ri],
randomNumbersY[ri], textAnchor );
ri = (ri+1) % RANDOMCOUNT;
}
oldFrameTime = frameTime;
}

我们现在用 StringBuffer 来绘制消息的字符。在 StringBuffer 结尾添加字符,比在开头插入要容易,因此我们调整了显示文字,frameTime 放到了消息的结尾,即“ms per frame:120”。这样我们每次只是更改 frameTime 的几个字符,而消息的文字则原封不动。像这样使用 StringBuffer,能够让系统不至于在每次循环的时候都要创建和销毁 String 对象。这是额外的工作,但是值得。注意我把 frame 强转成了 int,因为我发现使用 append(long) 会导致内存泄漏。我不知道为什么,不过这是很好的一个例子说明为什么在使用工具的时候要用眼睛盯好。

我们还用了 font.charsWidth() 来计算消息图像的宽度,这样我们能够尽量少做绘制的工作。我们使用了一个等宽的字体,因此图像“ms per frame:1”会比“ms per frame:888”小,而我们又在使用 Graphics.setClip(),因此我们不需要绘制多余的部分。这也意味着我们需要绘制一个足够大的矩形来清空我们需要的区域。当然,我们希望在绘图上节省的时间比因调用 font.charsWidth() 而多花的时间要多。

可能这里并不会有太大的意义,但是对于在游戏中在屏幕上绘制玩家的分数来讲,这是一项非常重要的技术。在这个情况下,绘制分数 0 和 150,000,000 是很不一样的。这里受到了 font.getBaselinePosition() 实现的错误返回值的阻碍,因为本来应该返回和 font.getHeight() 相同的值。唉~!

最后我们来看一下用两个数组存放的预计算的“随机”坐标,使得我们不用在循环中进行随机调用。注意这里用了一个取模操作来实现闭合的数组。同时注意我们现在使用 textAnchor 来绘制图像和字符串,这样 setClip() 才能正确工作。

根据这个版本的代码所产生的数字,我们目前处于一个比较尴尬的境地。描述器告诉我,现在 paint() 方法比没有做这些改变时,多花大约 7% 的时间。这可能得怪对 font.charsWidth() 的调用,因为它占用了 4.6%。(这并不太多,但是应该可以被减少。注意我们每次都在取得 MESSAGE 字符串的宽度,实际上我们可以在循环体之前就轻易的计算出,然后再加上 frameTime 的宽度。)同时,对 setClip() 的调用标示的是 0.85%,而花在 drawImage 上的时间看起来增加的比较显著(从 27.58% 到 33.94%)。

基于这一点,看起来当然是这些增加的代码把我们的程序变慢了,但是程序生成的值却和这个假设相矛盾。模拟器上的图表很不稳定,因此如果没有更多的测试我们无法得到确定的结论。然而,我的 i85s 却报告说,加上这些代码后,程序变快了一点点。只调用 setClip() 或者 charsWidth(),结果为 37130ms,而两个都调用得话,结果是 36540。在我耐心能够坚持的情况下,我把这个测试运行了很多遍,而结果是相同的。这也再次强调了不同的运行环境会有不同的结果。当你到了某一点无法确定是否做了改进的时候,你可能被迫在硬件上继续所有的测试,这需要对 JAR 文件进行很多安装和卸载工作。

那么看起来我们从常规的图形操作上挤出了不少的性能。现在是时候对我们的 work() 方法实施高端和低端优化的时候了。先让我们来复习一下这个方法:
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;
}

每次进入循环的时候,我们都会传入我们的数组。work() 方法中外层的循环计算我们的除数(divisor),然后调用 workMore() 来实施除法。可能已经发现,这里整件事情都有问题。开始的时候,编程的把对 getDivisor() 的调用放到了内层循环。由于 j 的值在内层循环过程中并不发生改变,因此除数也不发生变化,这完全应该放在内层循环之外。

如果我们再多考虑一点。这个调用本身也是完全没有必要的。以下代码可以做同样的事情……
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;
}

现在我们的描述器报告说我们的 run() 方法花费 23.72% 的时间,而我们做这些改进之前,这个值为 38.78%。在陷入低端优化把戏之前,请总是先用头脑来优化。不过,说到这里,还是让我们来看看这些小把戏。

(一) (二) (三) (四) (五)

Monday, March 13, 2006

J2ME 游戏优化探密(二)

哪些地方需要优化——九一原则

在性能较低的游戏中,90% 的时间被花费在运行 10% 的代码上。这 10% 的代码正是我们需要集中所有努力优化的地方。我们用一个描述器(profiler)来找出这 10% 的代码。要打开 J2ME Wireless Toolkit 中的 Profiler Utility,可以在 Edit 菜单中选择 Preference 项,打开 Preference 窗口,选择 Monitoring 标签,勾选标有“Enable Profiling”的复选框,然后点击“OK”按钮。没有任何事情发生?没关系——我们需要在模拟器中运行我们的程序,然后退出,这样 Profiler 窗口才会出现。现在就试试看吧!

Figure 1. 演示如何打开 Profiler Utility


我的模拟器(运行在 Windows XP 下,Intel P4 2.4GHz CPU)报告 100 次循环花费了 6,407ms,即 6 秒半种,每帧 61-63ms。在硬件(Motorala i85s)上,它运行慢许多。没帧的时间在 500ms 左右,而整个程序跑了 52460ms。在这篇文章种,我们将尝试改进这一现象。

当你退出程序时,描述器窗口将弹出来这样你就能看到一个类似于文件夹浏览器的东西,左边面板有一棵树,它分级显示了方法之间的关系。每个文加夹都是一个方法,打开一个方法的文件夹显示所有该方法调用的其它方法。在树中选择一个方法,将在右边窗口中显示该方法的描述信息以及所有其它被它调用的方法。注意每个元素附近显示有一个百分数。这个是该方法运行花费的时间占总运行时间的百分比。我们需要浏览这棵树,找出时间到花到哪里去了,然后尽可能地优化那些占有最高百分比的方法。

Figure 2. Profiler Utility 调用表单


关于描述器,有些地方需要注意。首先,你所得到的百分比可能和我的会不同,但是方法类似——总是追踪那些最大的数字。我每次运行程序,这些数字都不同。在做测试时,请尽可能地保持环境统一,你可能需要关掉所有的后台应用程序,比如电子邮件客户端等等。同时,在使用描述器之前也不要采取任何代码保护措施(如某些让源代码变混乱以防被反编译的工具),否则你的方法会被莫明奇妙的命名为“b”、“a”或者“ff”之类。最后,描述器本身对性能没有任何帮助,不管你模拟什么设备。硬件本身是完全不同的东西。

打开具有最高百分率的文件夹,我们发现 66.8% 的运行时间被名为 com.sun.kvem.midp.lcdui.EmulEventHandler$EventLoop.run 的方法消耗,这对我们帮助不大。继续向下挖掘一两层具有类似奇怪名称的方法文件夹,你将追踪到大的百分比方法 serviceRepaints(),然后最终到我们的 OCanvas.paint() 方法。另外 30% 的时间被它消耗。这两个方法都存在于游戏的主循环终,这并不奇怪——我们不会花费时间去优化 MIDlet 类中的代码,同时你也不会去为优化游戏主循环之外的代码费心。只优化那些反复运行的部分。

我们例子中这些百分数的分配和真实游戏的情况不会差太远。你会发现,一个真实视频游戏的绝大部分运行时间的大部分都被花费在 paint() 方法上。和非图像处理的部分比起来,图像处理的常规操作会花费非常多的时间。遗憾的是,图像处理的常规操作早已封装在 J2ME API 之下,在这一点上,我们没有多少余地用来改善其性能。我们所能做的是,聪明地决定我们用它们中的哪一个,以及如何使用它们。

高端优化 VS. 低端优化

本文后面将讨论有关低端优化技术。你将发现它们很容易应用到已存在的代码中,虽然会降低代码的可读性,但是会带来性能的提升。在你使用这些技术之前,最好先改善你代码的设计以及规则(algorithms),也就是高端优化。

Michael Abrash,id software 力作 Quake 开发者之一,曾经写到,“最好的优化工具,在你的双耳之间”。一个问题总有不止一个解决办法,如果在行动之前花些时间来考虑正确的方法,则可以达到事半功倍的的效果。使用正确的(比如最快的)规则体系,对性能的帮助将远远大于用低端技术来改进二流的设计带来的效果。用低端技术你可能可以降低几个百分点,但是之前请从高端优化开始,多用你的脑子——它就在你的双耳之间。

那么,让我们来看看我们在 paint() 方法里面干了些什么。每次循环我们调用了 16 次 Graphics.drawString() 方法用来在屏幕上显示“n ms per frame”。我们并不知道 drawString 方法内部的工作原理,但是我们知道它被用了很多次,于是让我们尝试一下另一种途径。我们可以把字符串直接一次性画到一个 Image 对象中,然后将该 Image 绘制 16 次。
public void paint(Graphics g) {
g.setColor( COLOR_BG );
g.fillRect( 0, 0, getWidth(), getHeight() );
Font font = Font.getFont( Font.FACE_PROPORTIONAL,
Font.STYLE_BOLD | Font.STYLE_ITALIC,
Font.SIZE_SMALL );
String msMessage = frameTime + "ms per frame";
Image stringImage =
Image.createImage( font.stringWidth( msMessage ),
font.getBaselinePosition() );
Graphics imageGraphics = stringImage.getGraphics();
imageGraphics.setColor( COLOR_BG );
imageGraphics.fillRect( 0, 0, stringImage.getWidth(),
stringImage.getHeight() );
imageGraphics.setColor( COLOR_FG );
imageGraphics.setFont( font );
imageGraphics.drawString( msMessage, 0, 0,
Graphics.TOP | Graphics.LEFT );
for ( int i = 0 ; i < DRAW_COUNT ; i ++ ) {
g.drawImage( stringImage, getRandom( getWidth() ),
getRandom( getHeight() ),
Graphics.VCENTER | Graphics.HCENTER );
}
}

当我们运行这个版本的程序时,我们会发现在 paint() 方法上花费的时间的百分率变小了一点点。更深入的观察我们会发现 drawString 方法仅被调用了 101 次,而 drawImage() 方法做了大部分的动作,被调用了 1616 次。尽管如此我们还是有成效,程序跑得更快了,因为我们使用的图形调用更快。

你可能会注意到将字符串绘制到图像会影响显示,因为 J2ME 并不支持图像的透明处理,因此很多背景被覆盖。对于前面所提到的,优化会导致你对应用的需求重新进行评估,这便是一个很好的例子。如果你真的需要文字的重叠,那么你可能就不得不去处理较慢的运行速度。

这端代码可能会稍好一些,但是仍然有很多优化的空间。接下来我们一起看看我们的第一个低端优化技术。

(一) (二) (三) (四) (五)

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

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

(一) (二) (三) (四) (五)