Friday, August 18, 2006

模版更新

看了 Blogger Beta 新的 Archive 树状的列表方式,非常喜欢,于是花了点时间,把自己现在的模版也改成了这个样子。刚好做 Calender 的时候,已经有一份完整的时间、帖子标题和地址的对应数据,所以做这个树状的 Archive 列表,只是表现的问题而已。不过,尽管如此,还是花了我好几个小时的时间。

另外,打开了 Blogger 的 Backlink 功能。由于要改模版,Blogger 提供这个功能以后一直没有动,嫌麻烦。不过,把这个功能打开容易,要把样式调整的和整体一致,还是花了不少功夫。

弄好后,把自己比较得意的帖子都看了一下,除了 Throwing Tables Out the Window 那篇以外,别的帖子几乎没什么 backlinks,可能还是因为我写的东西都比较私人吧。

Wednesday, August 16, 2006

Blogger 新动向

这两天,久经抱怨的 Google Blogger 服务终于有了新动静。从一年多以前开始,Blogger 就承诺对其提供的 Blog 服务进行改进,但除了新增了评论发布的校验码以及评论内容评审以外,未见有什么大的动作。前天,Blogger Buzz 发布了最新推出的 Blogger in beta,针对长期以来用户的抱怨提供了很多新功能,具体包括:
  • 可使用标签对文章进行分类
  • 对 Blog 的访问进行权限控制
  • 用鼠标拖拽即可完成对 Blog 模版的修改和设定,无须进行 HTML 编辑

然而,该项服务目前仅对一部分老用户开放,Blogger 声称会对所有逐步开放。如果你登录后 Dashboard 上有相关连接,那就说明你的帐号已经支持新功能;否则,就要再耐心地等上一段时间。

如果等不急,也可以立即注册一个新的 Beta 帐号,或者用 Google 的帐号,或者原来的 Blogger 帐号登录后创建一个新的 Blog 即可。Blogger 稍后会提供功能,让用户把原来的 Blog 和 Beta 帐号下的新 Blog 合并起来,方便管理。

Blogger 还提供了一个 Tour 来介绍新的功能,包括灵活定制模版、对访问进行权限控制、更多的 Feed 选项、更快捷的发布等。

我也创建了一个新的 Blog 来测试 Blogger Beta 的新功能。前前后后把它的控制面板翻了一遍,发现功能并没开发完全,有些地方还是不尽如人意:
  • 发布选项中,目前仅支持发布到 *.blogspot.com,尚未提供对 FTP 和 SFTP 发布的支持。
  • 创建帖子的时候,表单下面多了一个地方可以设置标签,用来对帖子进行分类。分类的内容会自动发布到帖子后面,而连接指向 http://*.blogspot.com/search?label=label,而并非静态的 HTML 支持。当然,对于分类和标签支持,如何访问指定标签相关的文章,这是最麻烦的一部分,但如果对某一个标签的访问是通过 CGI 程序动态支持的话,不知道对于通过 FTP 发布到指定 host 的用户,Blogger 将如何提供这一功能。
  • 对帖子进行回复仍然要访问 Blogger,表单完全脱离模版。自定义 HTML 提供回复表单,仍然无法使用 Word Verification 功能。
  • 还不支持对模板直接进行 HTML 编辑,也无法去掉页面顶上的 Navbar;仅能够把目前的模板降级为旧版本的模版进行编辑。

不过,我上周才决定要自己对 Blogger 进行 DIY,Blogger 这周就给出升级的消息,同时还包括标签和分类的功能,让我着实很郁闷。Blogger 马上就要自己支持的功能,我就完全没有必要自己 DIY 了阿。虽说是自己写点东西来玩,但也总要有用才有成就感啊。

在放出 Blogger 功能全面升级的消息后,昨天 Blogger Buzz 又放出了 Blogger API 更新的消息,这意味着:
  • Blogger/GData 文档中能够找到很多范例代码。
  • 提供 C# 和 Java 版本的客户端类库下载。
  • 对于账户校验,分别提供了针对桌面程序和 WEB 应用程序的详细文档。
  • GData 相关的代码将能够被很容易的重用于其它 GData 应用程序,如 Google 的 Calendar。

总的来说,这次 Blogger 算是给了支持者们一个值得期待的回应,虽然有很多地方还可以更好,不过也总比什么动静都没有好多了。

与此同时,Blogger 的默认托管站点域名 *.blogspot.com 最近可以在国内自由访问了。以上我建立的测试 Blog 就是用的这个域名。Google 上可以搜索到很多关于 Blogger 解封的信息,不过大家对 Blogger 是否真的解封众说纷纭,并没有确切的消息。这个域名被封锁长达三年之久,如今如果真的被解封了,当然是值得高兴,或许广大的 Google 和 Blogger 们暂时还无法相信这突如其来的幸福吧。

或许是 Google 为了在国内大力推广其 Blogger 服务,在着手升级 Blogger 功能的同时也大力加强公关工作让 *.blogspot.com 在国内真的解封;也有可能是 Blogger 升级让 *.blogspot.com 域名换了 IP 地址,从而暂时绕过了 GFW 的封锁,因为现在 *.blogspot.com 的域名和以前不同了。之前也发生过 Blogger 临时更换 IP 地址导致 Blogger 下站点在短时间内可以访问的事件。

虽然我自己没有用 *.blogspot.com,不会受封锁的影响,但是,解封也意味着可以看到更多 Blogger 上优秀的 blog。希望这次真的是解封了……

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

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

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

Tuesday, November 22, 2005

SQL Server 2000 数据库日志

SQL Server 2000 数据库对数据的事务会记录日志,并且能够根据日志回滚数据库操作,这个想必大家都知道。不过,如果日志变得很大,或者达到了限定的日志文件上限,就会对数据库产生较大的影响。

先来看看 SQL Server 2000 数据库日志文件的两种设定方式。


右下角可以设置日志文件的大小,上面一个选项是可以无限增大,后面一个选项是可以指定一个允许的最大值。数据库在运行过程中,这个日志文件会不断的增大,增大的速度视提供服务的压力而定。当这个文件达到预设的最大值时,任何数据库操作都会失败,会提示“无法写入日志”之类的报错信息;也可以允许这个文件无限增大,不过,当文件大到一定的程度,写入的效率会很低。笔者曾经遇到这个文件涨到 2.5 GB,在查明原因以前,整个系统莫名其妙效率降低很多,最后查到瓶颈在数据库。各种优化手段用上未果,最后发现日志是设置成无限增长的。把日志删除并把日志文件缩减后,问题解决。

因此,无论是让文件无限增长,还是设置文件最大值,都不可避免的遇到日志的删除和缩减问题。要缩减数据库日志,可以用查询分析工具 (SQL Query Analyzer) 运行如下命令:
DUMP TRANSACTION [db_name] WITH NO_LOG

然后如下图用“收缩数据库”(Shrink Database) 工具将删除后留下的空白空间收回。具体的:




然后选择“Files...”:


在“Database file”中选择日志对应的文件,一般是“[db_name]_Log”,“Shrink action”选择的第 4 项,将缩减的值设定为目前允许的最小值,然后点击“OK”即可。

Thursday, November 17, 2005

Tomcat 5.5 无法编译 Java 1.5 语法 JSP

Apache Jakarta Tomcat 5.5.x (笔者在 5.5.9 和 5.5.12 下遇到同样问题) 在安装过后,用默认配置,无法自动编译带有 Java 1.5 语法的 JSP,经过多方求证,问题是由其自带的 eclipse 编译器造成的。文档 http://jakarta.apache.org/tomcat/tomcat-5.5-doc/jasper-howto.html 中说:
The Java compiler from Eclipse JDT in included as the default compiler. It is an advanced Java compiler which will load all dependencies from the Tomcat class loader, which will help tremendously when compiling on large installations with tens of JARs. On fast servers, this will allow sub-second recompilation cycles for even large JSP pages. This new compiler will be updated to support the Java 5 syntax as soon as possible.

Apache Ant, which was used in previous Tomcat releases, can be used instead instead of the new compiler by simply removing the common/lib/jasper-compiler-jdt.jar file, and placing the ant.jar file from the latest Ant distribution in the common/lib folder. If you do this, you also need to use the "javac" argument to catalina.sh.

由此可见,这个编译器 common/lib/jasper-compiler-jdt.jar 是不支持 Java 5 语法的,而 Apache Ant 工程中的编译器可以替代它解决这个问题,具体方法如下:
  • 将/common/lib/common/lib/jasper-compiler-jdt.jar 删除
  • 将 ant-1.6.x 发布的 ant.jar 拷贝到 /common/lib/
  • 修改 /conf/web.xml 文件,找到并修改如下内容,增加 compilerSourceVM 和 compilerTargetVM 两个设置:
<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>

<init-param>
<param-name>compilerSourceVM</param-name>
<param-value>1.5</param-value>
</init-param>
<init-param>
<param-name>compilerTargetVM</param-name>
<param-value>1.5</param-value>
</init-param>

<init-param>
<param-name>fork</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>xpoweredBy</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>

Sunday, November 13, 2005

建立“月蝎研究”分舵

上上个星期起心思想改版,今天终于抽空将技术方面的文章独立出来单独开了一个 Blog,名字暂且就叫“月蝎研究”,以后有灵感了再改。模板风格也没有换,现暂且这样吧,和“今日无大事”保持风格统一,也挺好的。

如果算上兼职的话,工作也有五、六年了,从一开始做纯静态 WEB,到后来研究 PHP (也做过一点 ASP,但不熟),到改行做 Java,同时分管配置管理 (SCM),到后来做系统设计,慢慢地还是积累了不少东西,也得到了不少同事朋友的帮助。做这个 Blog,一方面是想把平时觉得重要的东西总结出来和大家分享讨论,算是一个学习笔记;另一方面,也会摘抄一些相关的好的文章,以使得更多的人受益。

现在 blog.windia.net 这个域名在 Google 的 PageRank 已经达到了 5,很多关键词的搜索都非常靠前,从流量分析上来看,像“青城后山”、“石象湖”、“document.write()”、“月亮落在天蝎座”这些关键词的搜索结果都在第一页甚至第一位,因此为了巩固这个效果,让更多的人能够找到这里,让流量更加集中,所以没有单独开另外的二级域名,而是只开了一个子目录,用 http://blog.windia.net/tech/ 来访问,同时,RSS Feed 订阅的地址为 http://feeds.feedburner.com/gregtech,仍然是 Blogger 一贯的 ATOM 格式,新老朋友们多多支持哦…… ^_^

Tuesday, October 18, 2005

SSI 和 document.write()

这两天一直在考虑怎么给 Blogger 加上 Calendar 和分类的功能。原来是这么打算的:

首先,要让生成的 HTML 代码尽量符合 XHTML 规范,以便可以写程序用 XML 解析器来分析并从中提取数据。只要是合法的 XML,应该问题就不大,不一定要完全遵循 XHTML 的 DTD,反正 Dom4J 没有 DTD 照样能分析 XML,不过,这个还有待试验证实。

Blogger 的帖子数据里面,link 这个字段似乎没有什么用,可以利用它来存储分类。通过程序分析提取以后,套用 Blogger 的模板生成 HTML,并把出现过的分类列表写到一个单独的 HTML 中,由需要的页面通过 SSI include 进来。

同样利用分析出来的数据,生成日历的 HTML,每个月一个文件,供需要的页面调用。

不过,要让 Resin 支持 SSI 功能,可能性不太大,必须另外想办法。

昨天给 BlogBus 做模板,发现这个 Blog 的默认模板里头,所有侧边栏(日历,最近贴,存档,Tags 等)全部都是由 JavaScript 来 document.write() 的,而 JavaScript 代码本身,也貌似是发布的时候根据数据动态产生的,估计这个是为了达到这些数据块可以在需要的地方重复调用的目的。

于是,我想,是不是我也可以将 Blogger 里头需要的 Calendar 和分类数据也在发布时用 Servlet 写成 JavaScript,然后再在需要的地方用这些 JavaScript 来 document.write()

嗯,有空试验一下就知道了…… 理论上应该是可行的……

Updated on 11/13/2005:

经过试验,用 document.write() 方式输出是可行的,现在此 blog sidebar 上的日历就是用 document.write() 打印出来的,而数据是由 Servlet 分析生成的 HTML 文件后自动写的 JavaScript 代码。问题有两个,一是用 Dom4j 解析的时候,对实体 (entities) 的解析有点问题,要把除 &amp; 以外的所有实体全部清楚掉才能解析正确,应该有正确的识别方法,尚在研究中。另一个问题是,documents.write() 写出来的内容,无法被搜索引擎识别,但对网站本身功能影响不大。

Monday, October 10, 2005

IE, Opera 和 Mozilla Firefox

为了验证 tableless layout 版面的浏览器兼容性,特地找来了时下流行的 Opera 8.5 和 Mozilla Firefox 1.0.7 与 微软的 Internet Explorer 6.0 一起测试,结果是倍受打击……

原来以为基本上不用改就能兼容,因为完全没有用 table 排版,结果在这两个非 IE 的浏览器上,页面虽然没有面目全非,但是也不如在 IE 上显示的完整,而且有些地方莫名奇妙的怎么都设置不正确。花了 4 个小时,经过反复的和 stopdesign 网站样式表的比对,最后发现原因有两点:

  • 没有用标准的 XHTML 头来声明页面的 DocType,经过测试,发现用传统的 HTML 4.0 声明,和 XHTML 声明,三种浏览器渲染出来的结果都不同;
  • Mozilla Firefox 浏览器对样式表的要求非常严格,所有的数字属性,除了 0 以外,都必须带上单位,否则一律忽略不计!为了这个我纳闷了很久——前两个浏览器都能正确的显示各种样式的 border,而 Firefox 上所有的 border 全部都被忽略了,汗哪…… 给 border-width 参数加上单位 px 后,就正确了。paddingmargin 具有同样的问题。

以上提到的 XHTML 需要如下声明:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0
Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://gmpg.org/xfn/1">

对于线条的样式,dotted 和 dashed,在 IE 上 width 为 1 时,都显示为 dashed;但这两个样式在 Opera 和 Firefox 上能够明显的区分出来。另外,原来几乎所有的 JavaScript 在 Mozilla Firefox 上都不能运行,它对 JavaScript 的语法要求也非常严格,必须完全符合 W3C 标准:
  • document.all['id-name'] 不合法,用 document.getElementById('id-name') 替代;
  • object.all.tags("DIV") 不合法,用 object.getElementsByTagName("DIV") 替代;
  • collObjs(i) 不合法,用 collObjs[i] 替代。

做完以上工作后,终于页面能够在三种浏览器上得到几乎相同的结果了。突然觉得自己做了 5 年的 WEB,完全被微软毒害了,已经养成了太多不好的习惯;看来以后要严格执行 W3C 的 XHTML、CSS 和 JavaScript 标准才是。