文章来源:J2ME开发网
上一次,我主要聊了一下当前手机游戏开发的平台选择和开发环境的安装,也具体的谈了谈开发J2ME程序的简单入门方法。由于当前在手机上做游戏开发的大部分同志们用的都是J2ME,所以朋友们又催我继续深入谈一谈J2ME的开发技巧。本人所学甚浅,许多地方也都只是触及皮毛,因此只能简单的谈谈我在手机游戏的开发中碰到的一些问题和我个人采用的解决方案。另外我把平时在论坛里收集到的部分技巧提供给大家以作参考,这里特别感谢那些无私奉献自己知识的人们。如果在文章中存在什么错误,还请各位老鸟海涵。希望我这篇文章能起到一点抛砖引玉的作用吧。
开发技巧这个东西是颇不好谈的。还记得我最早学习J2ME的时候,曾经在SUN的网站上看到过一篇文章,题目是《如何提高J2ME程序的效率》,作者口气强烈的要求j2me代码"寸土寸金",仿佛令我回忆起了当年在单片机上的编程,其中有几个观点到现在还记得,比如不到万不得已不要创建新的类,限制使用接口数量,还有要缩短变量或函数名称等等...我刚开始照着这个那个规范编程的时候,反而在许多地方束缚了自己,搞的这也不好做,那也不好办。呵呵,这大概就是尽信书不如无书吧。所以,我在聊技巧的时候不会给大家设置很多条条框框,只讲一下对某个问题通常的解决方法,希望能对大家有一点帮助。再次感谢那些在网上和论坛上给我提供过帮助的朋友们。
游戏贴图
说起来千头万绪,不如就从最常用的贴图技巧开始说起吧。
MIDP手机程序的标准图片格式是PNG(便携式网络图片格式)。这里值得注意的是,不同的手机平台对于图片的要求也满"挑食"的,同样是PNG格式却不一定适用于所有平台。我就碰到过这样的情况,利用WinXP自带画图板生成的PNG格式图片,在WTK的标准模拟器上可以正常显示,到了西门子模拟器上却怎么也显示不出来。经过一番折腾,我在Photoshop中重新生成了新的PNG格式图片后才顺利的显示出。网上的一些朋友也曾问我,为什么在模拟器上运行正常的图片在真实设备上却无法显示。我也只能对他说多换几种生成图片的工具试试看喽。另外,因为图片资源会占用较大空间,所以应该尽量保证其尺寸小,数量少。用不同的编辑工具存储PNG位图时,其文件的大小会有很大的不同,你可以尝试多使用几种工具,选择其中存储最小的来使用。在这里我推荐一个工具:Image Optimizer。它可以在不影响图象品质的前提下将图象减肥,最高可减少50%以上,真的很神奇呦J
要把图片加载到你的应用程序中,需要调用Image.createImage()这个函数,并且需要做相关的异常处理,所以我一般会在MIDlet中定义一个工具函数,LoadImage()
具体代码如下:
//加载图片
public Image LoadImage(String path)
{
try
{
return Image.createImage(path); //成功则返回图片对象
}
catch(Exception e)
{
e.printStackTrace(); //不成功则打印错误信息并返回空值
return null;
}
}
如果有人问我,jar中什么是最占地方的?什么是最让你头疼的?我当然会毫不犹豫地说是图片,是PNG图片了。但是一个好的游戏又怎能少得了那些精美的图片呢?一个经常使用的窍门是将许多的图片文件合并到一个图片文件中来,这样可以在总体上减小将图片占用的空间。最有代表性的例子就是精灵动画了
在文件中载入这类大图像后,可以采用以下的方法来绘出动画的各个帧
g.setClip(x, y, FRAME_WIDTH, FRAME_HEIGHT);
g.drawImage(fiveMenImage, x - FRAME_WIDTH * frameNumber, y, Graphics.TOP | Graphics.LEFT);
其中 x,y 为您绘图的起始坐标,FRAME_WIDTH和FRAME_HEIGHT为大图像的宽度和高度,frameNumber值由0-7的循环。这样你就可以制造出一幅精灵正在行走的动画了。但要注意,如果还有其他的图片需要绘制,请重置你的剪辑窗口。
此外,当然是地图的绘制了
我们的大地图,通常是由许多的小块拼出来的,并会用一个数组来保存地图各个位置的地形和状态,然后统一的在paint方法中刷出整张地图来。
实例代码如下:
HouseVector = new Vector(); //设置一个动态数组存放截下来的图片
Image ImageTemp = null; //存放大图片
Graphics g1 = null;
try
{
ImageTemp = Image.createImage("/res/image/house.png"); //加载整张大图片
}
catch(Exception exception) { }
for(int i = 0; i < 3; i++)
{
for(int j = 0; j < 4; j++)
{
Image image_element = Image.createImage(16, 16);//作为截图的小图片
g1 = image_element.getGraphics(); //获取小图片的图形设备
g1.drawImage(ImageTemp, -16 * j, -16 * i, 20); //开始截图
HouseVector.addElement(image_element); //添加到图片数组中
}
}
ImageTemp = null;
g1 = null;
此后你的HouseVector中就是一块一块在大图片中截取下来的矩形小图片了。以后按照预先设计好的地图数组,直接刷图就是了,你的游戏地图不就pp的秀出来了么?J
绘图技巧
我们通常做游戏是免不了需要使用Canvas类的,他就像一张空白的画布,我可以在上面描绘出我们的游戏主画面。但是游戏是一个连续动画的过程,而许多动画效果的优劣都取决于低级Canvas类的执行速度。当游戏的某一状态改变时,我们都要刷新屏幕。这个时候,我们通常需要用到这个函数
repaint();
serviceRepaints(); //强制重绘,慎用!
但是,这样全屏绘制效率很低,在某些手机上效果很不好。所以如果仅需要改变屏幕中一小部分的话,也可以用以下的方法来请求重绘:
Canvas.repaint(int x, int y, int width, int height)
这样你仅需指定屏幕的特定区域,然后用paint方法重绘,效率会提高很多,还会保存计算结果。不过值得注意的是,如果你发了很多次的重绘请求而快于系统的处理速度的话,他就会合并一些处理的请求,并重绘了全屏幕,所以,请尽量不要在循环或者时序较小的定时器中调用repaint(int x,int y,int width,int height)。
在绘制某一个屏幕区域过于频繁的情况下(譬如动画效果),难免会发生"闪烁"的现象,造成这一问题的原因就在于在绘制屏幕某处之后的瞬间程序又在此处绘制了新的图像,解决这一现象比较有效的方法就是绘制缓冲图像,在内存开辟一块区域作为后台画面,游戏逻辑对它更新,在一次循环结束后再显示,这样就可以在避免出现因一次循环中对画面进行多次更新而产生的闪烁现象,另外请特别注意你在repaint中设定好的矩形范围。
具体的操作方法如下:
public class MyCanvas extends Canvas implements Runnable
{
Graphics bg; //缓冲区图像设备
Image buf; //缓冲区图像
public MyCanvas()
{
......
drawOthers();
int height = getHeight();
int width = getWidth();
buf = Image.createImage(width, height); //按显示屏幕大小建立缓冲对象
//此处也可以设为重复绘制的矩形区域
//将缓冲图像的图形设备赋给bg
bg = buf.getGraphics();
......
}
public void run()
{......
for(i=0;i {
for(j=0;j {
drawBlock(x,y);//在缓冲区内绘制图像
}
}
repaint();//将缓冲区的图像重绘到屏幕上
}
private void drawBlock(int block_x, int block_y)
{
//取得方块的坐标
int x = getLeft(block_x);
int y = getTop(block_y);
//取得方块的颜色
int c= board[block_x][block_y];
bg.drawImage(imgs[c], x, y, Graphics.TOP | Graphics.LEFT); //在缓冲区的图 形设备中循环绘制图像
}
public void paint(Graphics g)
{
g.drawImage(buf, 0, 0, Graphics.TOP | Graphics.LEFT); //绘制缓冲区图像
}
}
我在游戏中的地图就是采用这种方法来绘制的。J
闪屏和进度条
你的程序有时候会由于处理一些比较复杂的工作(尤其是初始化)而导致整个屏幕静止了下来,不耐烦的用户就开始使劲的揿手机的键盘了。毕竟是手机程序嘛,如果让用户看到游戏的显示已经停止了变化,他们就一定会怀疑游戏或者手机发生了故障。所以我们要想办法告诉用户当前程序正在运行,这就涉及到了延迟掩盖技术。方法有很多,最常用的是闪屏(Splash Screen)和进度条方法。
如果你的游戏不能迅速的反应出用户的按键操作,则会被感应到响应迟钝。其实这种响应的速度与不同型号的手机是有很大关系的。当然了,任何超过一秒的操作都会被用户认为是极为漫长的,所以无论是何种型号的手机,都必须要确保按键事件回调函数(keyPress和commandAction)的快速返回。因为负责重绘的线程可能也会调用按键函数(而重绘线程又往往是最耗时的),所以应该启动一个独立的线程,让用户看到他的操作有所回应。
多线程被认为是解决此问题的一个合适的方法。这是因为当你的一个线程正处于某种条件等待的时候,另一个线程仍然能工作。需要注意的是,Java线程不一定会具有抢先权,所以我们的代码不应该在死循环中去等待条件,这样常会被用户错误地认为是死机了。遇到实时性比较高的游戏我们可以在每次的循环中调用yield或者wait方法。
例如:
while (!stopped)
{
doSomething();
synchronized(this)
{
wait(500); //milliseconds, i.e. half a second
}
}
使用资源
合理的资源利用,也是保证程序高效的有效手段之一。其中最为主要的就是合理使用你的堆内存,必须确保释放不再需要的Form和Canvas,(例如Splash Screen,游戏的关卡等)这些可都是些大家伙,极耗内存的,不用的时候可以将他们置为空,以便进行垃圾收集。不经常用的屏幕(如游戏说明,选项等)应该在用的时候才创建,并且在使用后别忘了作为垃圾收回。这样做也是牺牲了速度而换取了额外的堆空间。如何在时间和空间这对矛盾中进行取舍,可就要完全看读者你了。J
我们在写java文件的时候,经常要导入一些开发包,这些开发包给我们提供了各种各样的支持的库功能。譬如系统自带的 java.io.*或者 java.util.* 以及第三方厂商提供的扩充包com.nokia.mid.ui.FullCanvas等。但是如果整个库都被包括在Midlet文件中,你可能就要为许多程序本不需要的功能增加系统开销了。所以在使用库的时候,一定要清楚是否真正的需要用到库中所有的类。请尽量不要使用.*的模式,而是明确地指定您具体导入的包名称,例如com.nokia.mid.ui.FullCanvas。
在垃圾的对象负责收集管理的实例中,我不知道有什么比String和StringBuffer更加有名气的了。不止在一份资料上有这方面的例子。String 作为一个不可变对象,他的状态自创建后就不可改变了。这样虽然利于代码的可靠和线程的安全,但是当它所代表的值发生变化时,初始值就失去了意义,于是旧的对象滞留在了堆里,形成垃圾。多数程序员都应记得曾因为使用String而产生了不知道多少的垃圾对象,这对程序的效率会有很大影响的。所以要想进一步提高效率,最好使用可重用的对象。
示例代码如下:
经典的例子是字符串的连接。
请看下面的倒排字符串函数:
static String reverse(String s)
{
String t = "";
for (int i = s.length()-1; i >= 0; --i)
t += s.charAt(i);
return t;
}
第5行的赋值语句并不改变字符串t,因为t是不可变的。相反,它每次都创建一个新字符串,复制现存的值并添加新字符。这一方法将不必要地创建s.length()个垃圾对象。这个典型例子说明了不可变对象所存在的问题。然而,对于字符串却有一个简单的解决办法:类java.lang.StringBuffer是与String相对应的的可变操作类,上面的例子可以更高效地重写为: static String reverse(String s)
{
StringBuffer t = new StringBuffer(s.length());
for (int i = s.length()-1; i >= 0; --i)
t.append(s.charAt(i));
return t.toString();
}
这是一个经常被拿出来的范例。J
调试程序
我在开发中是选用WTK环境来进行编译打包的。写Code采用的是Uedit。这些工具配置起来极为简单,使用也很方便。可就是有一样,调试起来比较的困难,没有一个很好的DeBug工具。尽管WTK自带了根据设备定制异常跟踪、内存管理等功能,但用于调试程序还是不太方便。故此需要我们自己来想想办法。最经常用的当然是ShowMessage大法了,可以自己在程序中选择断点,把程序运行于此处时的各个变量值Print出来。
例如: System.out.print(程序的过程变量);
但是我有一个朋友自己写了一个设置断点的工具函数,也是非常好用的,共享给大家:
boolean debugflg=false;//调试标志
...
Debug() //设置断点
...
private void Debug ()
{
System.out.print(程序的过程变量);//打印程序的当前变量的值
while(debugflg){} //等待用户消息
}
public void keyPressed(int keyCode)
{
debugflg =false;
}
校屏
大家是否都有过这样的经验:在一个设备上作好的程序当拿到另外的设备上跑的时候, 即使程序中的API都是通用的,还是会出现这样那样的问题。其中最令人头疼的可能就是图片和文字都会因为设备的屏幕大小不同而乱掉。乖乖,又是好一番调整。J 如果想让自己写的程序能够畅通无阻的运行在各种平台上,第一是要考虑用通用的开发包,第二当然就是需要校屏,也就是将自己要显示的图片,文字用程序动态的确定位置,使之在各种大小的屏幕上都能达到居中等效果。这样才能保证程序的通用性。
实例如下:
public class MyCanvas extends Canvas
{
private final int addX; //坐标矫正
private final int addY;
private final int screen_X; //屏幕顶点
private final int screen_Y;
private final int str_X; //字符串顶点
private final int str_Y;
private final int pic_X; //图形的顶点
private final int pic_Y;
public MyCanvas()
{
//取得当前手机屏幕的高度和宽度
int height = getHeight();
int width = getWidth();
//坐标矫正量
addX = (width-120)/2;
addY = (height-142)/2;
//初始化屏幕参数
screen _X = addX + 48; //屏幕顶点
screen _Y = addY + 10;
str _X = addX + 19; //打印字符串的顶点
str _Y = addY + 103;
pic _X = addX + 36; //显示图像的顶点
pic _Y = addY + 34;
}
}
我记得一位老鸟曾经告诉过我:程序往往用90%的时间去运行10%的代码。因此,与其努力提高所有代码的效率,不如找出代码中的"瓶颈",使之更高效地工作,这样会得到更高的收益。呵呵,有时候细心的逐行审查自己的代码,往往会存在几条语句能大大的提高运行的效率。但是如果你想程序能够得到更高的效率,那么就请在写代码之前精心的设计和选用合适的算法把。J
校屏
大家是否都有过这样的经验:在一个设备上作好的程序当拿到另外的设备上跑的时候, 即使程序中的API都是通用的,还是会出现这样那样的问题。其中最令人头疼的可能就是图片和文字都会因为设备的屏幕大小不同而乱掉。乖乖,又是好一番调整。J 如果想让自己写的程序能够畅通无阻的运行在各种平台上,第一是要考虑用通用的开发包,第二当然就是需要校屏,也就是将自己要显示的图片,文字用程序动态的确定位置,使之在各种大小的屏幕上都能达到居中等效果。这样才能保证程序的通用性。
实例如下:
public class MyCanvas extends Canvas
{
private final int addX; //坐标矫正
private final int addY;
private final int screen_X; //屏幕顶点
private final int screen_Y;
private final int str_X; //字符串顶点
private final int str_Y;
private final int pic_X; //图形的顶点
private final int pic_Y;
public MyCanvas()
{
//取得当前手机屏幕的高度和宽度
int height = getHeight();
int width = getWidth();
//坐标矫正量
addX = (width-120)/2;
addY = (height-142)/2;
//初始化屏幕参数
screen _X = addX + 48; //屏幕顶点
screen _Y = addY + 10;
str _X = addX + 19; //打印字符串的顶点
str _Y = addY + 103;
pic _X = addX + 36; //显示图像的顶点
pic _Y = addY + 34;
}
}
我记得一位老鸟曾经告诉过我:程序往往用90%的时间去运行10%的代码。因此,与其努力提高所有代码的效率,不如找出代码中的"瓶颈",使之更高效地工作,这样会得到更高的收益。呵呵,有时候细心的逐行审查自己的代码,往往会存在几条语句能大大的提高运行的效率。但是如果你想程序能够得到更高的效率,那么就请在写代码之前精心的设计和选用合适的算法把。