十一月's profileNOVEMBREBlogListsGuestbookMore ![]() | Help |
|
|
January 31 MIDP游戏开发中处理文字的换行方案在游戏中,尤其是情景类的游戏当中,往往需要大量情节介绍的文字。要在小小的手机屏幕上显示这些文字,就必须对这些文字进行处理,使其能正确的换行,显示在你想要显示的宽度的范围内。下面我就会详细的介绍如何处理文字的换行。
首先应该计算需要换行的位置。这里我们以文字需要显示的宽度linewd,和“\n”为换行的标志 static public int ChangLine(String str, Font font, int linewd, boolean fullword) 计算好位置后,就开始为文字分行。 static public void DoLine(String infostr, int len) Brew手机开发资源听朋友提起 Brew手机开发,这个东东以前还比较陌生。 记录一下: http://www.cnjm.net/cgi-bin/lbcjm/forums.cgi?forum=30 另外转一篇brew学习的文章: 首先声明,我不是BREW 高手,我只是一位普通的BREW OEM 工作者.只是在自己不断学习BREW 的过程中,总结了一些学习BREW 的方法,或者说是要学好BREW 应该一步步怎么走,循序渐进的看些什么,学些什么,现在将这些心得体会和大家分享,交流。 1. 对BREW 有最基本,最浅显的了解(通过网站关于BREW 的新闻,介绍,对BREW 有个非技术角度的总体概念,知道他出现的用途是什么,在无线移动产业中处于什么位置,有什么优势等等,BREW 可以用来作些什么好玩的东东),激发对BREW 的初步热情. 2. 进行BREW 最基本的技术层次的学习(对BREW 最基本的事件驱动机制的了解,如何创建接口,clsid是什么?如何一步步用vc 开发一个动态应用,每一步骤的作用,resource,mif 工具的使用,helloworld程序如何被创建,如何进行事件处理的大致流程,如何将BREW 动态应用转变为mod 文件下载到手机.....等等等等,这些基本的内容可以通过一般的BREW 文档学习到,qualcomm 网站上有一篇很好的brew 基本知识学习文档,study brew from scratch,另外网上有两本关于BREW 的英文ebook,也可作为入门资料,有一本BREW 手机游戏开发的中文书也可以作为该阶段的入门资料) 3. 深入理解BREW 接口机制:BREW 采用面向对象的接口体系结构对外提供服务.理解接口机制将有助于理解调用所谓的接口API(实际上是宏映射)是如何最终调用到AEE 层的真正函数,定义一个接口究竟底层作了什么,BREW 中的接口究竟是如何创建的等等(该机制非常非常好的学习文档是aee.h 和example下的mediaplayer,aee.h 里的宏,就是接口机制的实现手段,mediaplayer 更是将brew 的接口机制发挥的淋漓尽致) 4. BREW 面向开发的核心机制的深入熟悉和理解:主要是事件分发,处理机制,Timer 机制,Alarm 机制,Notify 机制,Callback 机制,suspend,resume,background 等等 5.BREW 开发代码的熟悉,积累以及SDK API 的熟悉: 6.进行(你负责的或者是感兴趣的)BREW 模块(接口) 7.BREW 模块加载,应用启动机制学习:可以通过AEEModGen.c,AEEAPPGen.c 以及mod 文件的makefile 深入学习BREW App 的创建过程,Module 的加载过程 8.BREW 面向OEM 的核心机制学习:学习如何在Task 中启动BREW,BREW 事件如何分发,启动,关闭一个BREW应用的整个过程,oem_notify 机制,抽象接口机制,Interface Register 机制,BREW 分层概念,app stack,如何传递Key Event,App Context,Memory Manager,Object Manager,Callback,Systemcallback,SystemObject 等等面向OEM 层的BREW 核心机制 9.BREW 接口验证机制的学习: 主要是学习PEK 工具的使用,这样能经常性的来验证BREW 接口Porting 的完整性.同时PEK 中的OAT 源码也是学习接口使用的很好资料.通常,对于一个扩展的接口,需要提供扩展的OAT测试module,所以需要熟悉这些. 10.BREW UI 机制:主要学习BUIT(现在改名为BUIW)和UIOne.这两种机制主要用来进行UI 开发.由于BUIT 大量使用了设计模式,所以初学较难. 11.OEM Notes 的经常性关注:经常性登陆BREW OEMExtranet 下载BREW OEM Notes,有助于拓宽解决一些BREW Porting 的方法,思路,即便不是马上能用上,也积累了一些经验 January 30 j2me最佳联网方案终结版(1) .由于无线设备所能支持的网络协议非常有限,仅限于HTTP,Socket,UDP等几种协议,不同的厂家可能还支持其他网络协议,但是,MIDP 1.0规范规定,HTTP协议是必须实现的协议,而其他协议的实现都是可选的。因此,为了能在不同类型的手机上移植,我们尽量采用HTTP作为网络连接的首选协议,这样还能重用服务器端的代码。但是,由于HTTP是一个基于文本的效率较低的协议,因此,必须仔细考虑手机和服务器端的通信内容,尽可能地提高效率。 对于MIDP应用程序,应当尽量做到: 1.发送请求时,附加一个User-Agent头,传入MIDP和自身版本号,以便服务器能识别此请求来自MIDP应用程序,并且根据版本号发送相应的相应。 2.连接服务器时,显示一个下载进度条使用户能看到下载进度,并能随时中断连接。 3.由于无线网络连接速度还很慢,因此有必要将某些数据缓存起来,可以存储在内存中,也可以放到RMS中。 对于服务器端而言,其输出响应应当尽量做到: 1. 明确设置Content-Length字段,以便MIDP应用程序能读取HTTP头并判断自身是否有能力处理此长度的数据,如果不能,可以直接关闭连接而不必继续读取HTTP正文。 2. 服务器不应当发送Html内容,因为MIDP应用程序很难解析HTML,XML虽然能够解析,但是耗费CPU和内存资源,因此,应当发送紧凑的二进制内容,用DataOutputStream直接写入并设置Content-Type为application/octet-stream。 3. 尽量不要重定向URL,这样会导致MIDP应用程序再次连接服务器,增加了用户的等待时间和网络流量。 4. 如果发生异常,例如请求的资源未找到,或者身份验证失败,通常,服务器会向浏览器发送一个显示出错的页面,可能还包括一个用户登录的Form,但是,向MIDP发送错误页面毫无意义,应当直接发送一个404或401错误,这样MIDP应用程序就可以直接读取HTTP头的响应码获取错误信息而不必继续读取相应内容。 5. 由于服务器的计算能力远远超过手机客户端,因此,针对不同客户端版本发送不同响应的任务应该在服务器端完成。例如,根据客户端传送的User-Agent头确定客户端版本。这样,低版本的客户端不必升级也能继续使用。 MIDP的联网框架定义了多种协议的网络连接,但是每个厂商都必须实现HTTP连接,在MIDP 2.0中还增加了必须实现的HTTPS连接。因此,要保证MIDP应用程序能在不同厂商的手机平台上移植,最好只使用HTTP连接。虽然HTTP是一个基于文本的效率较低的协议,但是由于使用特别广泛,大多数服务器应用的前端都是基于HTTP的Web页面,因此能最大限度地复用服务器端的代码。只要控制好缓存,仍然有不错的速度。 SUN的MIDP库提供了Javax.microediton.io包,能非常容易地实现HTTP连接。但是要注意,由于网络有很大的延时,必须把联网操作放入一个单独的线程中,以避免主线程阻塞导致用户界面停止响应。事实上,MIDP运行环境根本就不允许在主线程中操作网络连接。因此,我们必须实现一个灵活的HTTP联网模块,能让用户非常直观地看到当前上传和下载的进度,并且能够随时取消连接。 一个完整的HTTP连接为:用户通过某个命令发起连接请求,然后系统给出一个等待屏幕提示正在连接,当连接正常结束后,前进到下一个屏幕并处理下载的数据。如果连接过程出现异常,将给用户提示并返回到前一个屏幕。用户在等待过程中能够随时取消并返回前一个屏幕。 我们设计一个HttpThread线程类负责在后台连接服务器,HttpListener接口实现Observer(观察者)模式,以便HttpThread能提示观察者下载开始、下载结束、更新进度条等。HttpListener接口如下: public interface HttpListener { void onSetSize(int size); void onFinish(byte[] data, int size); void onProgress(int percent); void onError(int code, String message); } 实现HttpListener接口的是继承自Form的一个HttpWaitUI屏幕,它显示一个进度条和一些提示信息,并允许用户随时中断连接: public class HttpWaitUI extends Form implements CommandListener, HttpListener { private Gauge gauge; private Command cancel; private HttpThread downloader; private Displayable displayable; public HttpWaitUI(String url, Displayable displayable) { super("Connecting"); this.gauge = new Gauge("Progress", false, 100, 0); this.cancel = new Command("Cancel", Command.CANCEL, 0); append(gauge); addCommand(cancel); setCommandListener(this); downloader = new HttpThread(url, this); downloader.start(); } public void commandAction(Command c, Displayable d) { if(c==cancel) { downloader.cancel(); ControllerMIDlet.goBack(); } } public void onFinish(byte[] buffer, int size) { … } public void onError(int code, String message) { … } public void onProgress(int percent) { … } public void onSetSize(int size) { … } } HttpThread是负责处理Http连接的线程类,它接受一个URL和HttpListener: class HttpThread extends Thread { private static final int MAX_LENGTH = 20 * 1024; // 20K private boolean cancel = false; private String url; private byte[] buffer = null; private HttpListener listener; public HttpThread(String url, HttpListener listener) { this.url = url; this.listener = listener; } public void cancel() { cancel = true; } } (2). 使用GET获取内容 我们先讨论最简单的GET请求。GET请求只需向服务器发送一个URL,然后取得服务器响应即可。在HttpThread的run()方法中实现如下: public void run() { HttpConnection hc = null; InputStream input = null; try { hc = (HttpConnection)Connector.open(url); hc.setRequestMethod(HttpConnection.GET); // 默认即为GET hc.setRequestProperty("User-Agent", USER_AGENT); // get response code: int code = hc.getResponseCode(); if(code!=HttpConnection.HTTP_OK) { listener.onError(code, hc.getResponseMessage()); return; } // get size: int size = (int)hc.getLength(); // 返回响应大小,或者-1如果大小无法确定 listener.onSetSize(size); // 开始读响应: input = hc.openInputStream(); int percent = 0; // percentage int tmp_percent = 0; int index = 0; // buffer index int reads; // each byte if(size!=(-1)) buffer = new byte[size]; // 响应大小已知,确定缓冲区大小 else buffer = new byte[MAX_LENGTH]; // 响应大小未知,设定一个固定大小的缓冲区 while(!cancel) { int len = buffer.length - index; len = len>128 ? 128 : len; reads = input.read(buffer, index, len); if(reads<=0) break; index += reads; if(size>0) { // 更新进度 tmp_percent = index * 100 / size; if(tmp_percent!=percent) { percent = tmp_percent; listener.onProgress(percent); } } } if(!cancel && input.available()>0) // 缓冲区已满,无法继续读取 listener.onError(601, "Buffer overflow."); if(!cancel) { if(size!=(-1) && index!=size) listener.onError(102, "Content-Length does not match."); else listener.onFinish(buffer, index); } } catch(IOException ioe) { listener.onError(101, "IOException: " + ioe.getMessage()); } finally { // 清理资源 if(input!=null) try { input.close(); } catch(IOException ioe) {} if(hc!=null) try { hc.close(); } catch(IOException ioe) {} } } 当下载完毕后,HttpWaitUI就获得了来自服务器的数据,要传递给下一个屏幕处理,HttpWaitUI必须包含对此屏幕的引用并通过一个setData(DataInputStream input)方法让下一个屏幕能非常方便地读取数据。因此,定义一个DataHandler接口: public interface DataHandler { void setData(DataInputStream input) throws IOException; } HttpWaitUI响应HttpThread的onFinish事件并调用下一个屏幕的setData方法将数据传递给它并显示下一个屏幕: public void onFinish(byte[] buffer, int size) { byte[] data = buffer; if(size!=buffer.length) { data = new byte[size]; System.arraycopy(data, 0, buffer, 0, size); } DataInputStream input = null; try { input = new DataInputStream(new ByteArrayInputStream(data)); if(displayable instanceof DataHandler) ((DataHandler)displayable).setData(input); else System.err.println("[WARNING] Displayable object cannot handle data."); ControllerMIDlet.replace(displayable); } catch(IOException ioe) { … } } 以下载一则新闻为例,一个完整的HTTP GET请求过程如下: 首先,用户通过点击某个屏幕的命令希望阅读指定的一则新闻,在commandAction事件中,我们初始化HttpWaitUI和显示数据的NewsUI屏幕: public void commandAction(Command c, Displayable d) { HttpWaitUI wait = new HttpWaitUI("http://192.168.0.1/news.do?id=1", new NewsUI()); ControllerMIDlet.forward(wait); } NewsUI实现DataHandler接口并负责显示下载的数据: public class NewsUI extends Form implements DataHandler { public void setData(DataInputStream input) throws IOException { String title = input.readUTF(); Date date = new Date(input.readLong()); String text = input.readUTF(); append(new StringItem("Title", title)); append(new StringItem("Date", date.toString())); append(text); } } 服务器端只要以String, long, String的顺序依次写入DataOutputStream,MIDP客户端就可以通过DataInputStream依次取得相应的数据,完全不需要解析XML之类的文本,非常高效而且方便。 需要获得联网数据的屏幕只需实现DataHandler接口,并向HttpWaitUI传入一个URL即可复用上述代码,无须关心如何连接网络以及如何处理用户中断连接。 (3). 使用POST发送数据 以POST方式发送数据主要是为了向服务器发送较大量的客户端的数据,它不受URL的长度限制。POST请求将数据以URL编码的形式放在HTTP正文中,字段形式为fieldname=value,用&分隔每个字段。注意所有的字段都被作为字符串处理。实际上我们要做的就是模拟浏览器POST一个表单。以下是IE发送一个登陆表单的POST请求: POST http://127.0.0.1/login.do HTTP/1.0 Accept: image/gif, image/jpeg, image/pjpeg, */* Accept-Language: en-us,zh-cn;q=0.5 Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1) Content-Length: 28 \r\n username=admin&passWord=1234 要在MIDP应用程序中模拟浏览器发送这个POST请求,首先设置HttpConnection的请求方式为POST: hc.setRequestMethod(HttpConnection.POST); 然后构造出HTTP正文: byte[] data = "username=admin&password=1234".getBytes(); 并计算正文长度,填入Content-Type和Content-Length: hc.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); hc.setRequestProperty("Content-Length", String.valueOf(data.length)); 然后打开OutputStream将正文写入: OutputStream output = hc.openOutputStream(); output.write(data); 需要注意的是,数据仍需要以URL编码格式编码,由于MIDP库中没有J2SE中与之对应的URLEncoder类,因此,需要自己动手编写这个encode()方法,可以参考java.net.URLEncoder.java的源码。剩下的便是读取服务器响应,代码与GET一致,这里就不再详述。 使用multipart/form-data发送文件 如果要在MIDP客户端向服务器上传文件,我们就必须模拟一个POST multipart/form-data类型的请求,Content-Type必须是multipart/form-data。 以multipart/form-data编码的POST请求格式与application/x-www-form-urlencoded完全不同,multipart/form-data需要首先在HTTP请求头设置一个分隔符,例如ABCD: hc.setRequestProperty("Content-Type", "multipart/form-data; boundary=ABCD"); 然后,将每个字段用“--分隔符”分隔,最后一个“--分隔符--”表示结束。例如,要上传一个title字段"Today"和一个文件C:\1.txt,HTTP正文如下: --ABCD Content-Disposition: form-data; name="title" \r\n Today --ABCD Content-Disposition: form-data; name="1.txt"; filename="C:\1.txt" Content-Type: text/plain \r\n <这里是1.txt文件的内容> --ABCD-- \r\n 请注意,每一行都必须以\r\n结束,包括最后一行。如果用Sniffer程序检测IE发送的POST请求,可以发现IE的分隔符类似于---------------------------7d4a6d158c9,这是IE产生的一个随机数,目的是防止上传文件中出现分隔符导致服务器无法正确识别文件起始位置。我们可以写一个固定的分隔符,只要足够复杂即可。 发送文件的POST代码如下: String[] props = ... // 字段名 String[] values = ... // 字段值 byte[] file = ... // 文件内容 String BOUNDARY = "---------------------------7d4a6d158c9"; // 分隔符 StringBuffer sb = new StringBuffer(); // 发送每个字段: for(int i=0; i sb = sb.append("--"); sb = sb.append(BOUNDARY); sb = sb.append("\r\n"); sb = sb.append("Content-Disposition: form-data; name=\""+ props + "\"\r\n\r\n"); sb = sb.append(URLEncoder.encode(values)); sb = sb.append("\r\n"); } // 发送文件: sb = sb.append("--"); sb = sb.append(BOUNDARY); sb = sb.append("\r\n"); sb = sb.append("Content-Disposition: form-data; name=\"1\"; filename=\"1.txt\"\r\n"); sb = sb.append("Content-Type: application/octet-stream\r\n\r\n"); byte[] data = sb.toString().getBytes(); byte[] end_data = ("\r\n--" + BOUNDARY + "--\r\n").getBytes(); // 设置HTTP头: hc.setRequestProperty("Content-Type", MULTIPART_FORM_DATA + "; boundary=" + BOUNDARY); hc.setRequestProperty("Content-Length", String.valueOf(data.length + file.length + end_data.length)); // 输出: output = hc.openOutputStream(); output.write(data); output.write(file); output.write(end_data); // 读取服务器响应: // TODO... (4). 使用Cookie保持Session 通常服务器使用Session来跟踪会话。Session的简单实现就是利用Cookie。当客户端第一次连接服务器时,服务器检测到客户端没有相应的Cookie字段,就发送一个包含一个识别码的Set-Cookie字段。在此后的会话过程中,客户端发送的请求都包含这个Cookie,因此服务器能够识别出客户端曾经连接过服务器。 要实现与浏览器一样的效果,MIDP应用程序必须也能识别Cookie,并在每个请求头中包含此Cookie。 在处理每次连接的响应中,我们都检查是否有Set-Cookie这个头,如果有,则是服务器第一次发送的Session ID,或者服务器认为会话超时,需要重新生成一个Session ID。如果检测到Set-Cookie头,就将其保存,并在随后的每次请求中附加它: String session = null; String cookie = hc.getHeaderField("Set-Cookie"); if(cookie!=null) { int n = cookie.indexOf(';'); session = cookie.substring(0, n); } 使用Sniffer程序可以捕获到不同的Web服务器发送的Session。WebLogic Server 7.0返回的Session如下: Set-Cookie: JSESSIONID=CXP4FMwOJB06XCByBWfwZBQ0IfkroKO2W7FZpkLbmWsnERuN5u2L!-1200402410; path=/ 而Resin 2.1返回的Session则是: Set-Cookie: JSESSIONID= aTMCmwe9F5j9; path=/ 运行Asp.Net的IIS返回的Session: Set-Cookie: ASPSESSIONIDQATSASQB=GNGEEJIDMDFCMOOFLEAKDGGP; path=/ 我们无须关心Session ID的内容,服务器自己会识别它。我们只需在随后的请求中附加上这个Session ID即可: if(session!=null) hc.setRequestProperty("Cookie", session); 对于URL重写来保持Session的方法,在PC客户端可能很有用,但是,由于MIDP程序很难分析出URL中有用的Session信息,因此,不推荐使用这种方法。 在MIDP2.0中调用平台服务在MIDP2.0中提供了非常重要的一项功能:调用外部平台的服务,比如从网络开始下载和安装一个MIDlet套件、发起话音呼叫。如果你做了一个通信录,那么就可以直接调用电话服务来呼叫一个号码。在MIDP2.0中可以通过MIDlet.platformRequest()方法实现上面的功能。
参考一下Javax.microedition.midlet.MIDlet.platformRequest()的java doc,你可以发现他只有一个参数为String类型的URL,当你调用他的时候,他会把这个URL传递给应用管理软件。应用管理软件来判断这个参数是否合理,是否存在相关的服务可以使用。如果服务可用并且需要MIDlet首先退出才能执行调用的服务的话,那么方法会返回一个boolean类型的true。如果服务并不存在的话会抛出ConnectionNotFoundExcepton。 在MIDP2.0的规范中,定义了两种服务类型: 在WTK2.1中提供了对这个方法的支持,你只需要配置一下就可以了。假设你的WTK的安装目录为WTK_HOME,那么进入WTK_HOME/lib,编辑system.config文件,在里面加上一句:com.sun.midp.midlet.platformRequestCommand: "C:\Program Files\MYIE2\MyIE.exe"。注意一定要 import javax.microedition.midlet.MIDlet;
}
}
使用platformRequest()自动更新MIDlet套件MIDP 2.0提供了一系列的新特性,其中一个就是调用平台的应用。例如可以调用WAP浏览器访问特定的网址,也可以调用电话应用程序呼叫某个号码。上述两个功能都可以使用MIDlet类的platformRequest()方法实现,platformRequest()接受一个String类型的参数url,如果是呼叫电话号码则url的形式为tel:13810011001。如果是调用WAP浏览器则url的形式类似于http://www.j2medev.com/wap.wml。 本文介绍如何使用platformRequest()方法自动更新MIDlet套件,在MIDP的文档中说明。如果url的形式为一个指定的MIDlet套件,可以是JAD文件,也可以是jar文件。例如http://www.j2medev.com/helloworld.jad。这个时候,此请求会被认为是安装MIDlet套件来对待,这样用户可以控制安装的过程,就像我们直接从WAP浏览器输入地址安装MIDlet套件一样。如果请求的MIDlet套件是当前正在运行的应用程序的升级版,则当前的程序需要先退出,然后执行更新操作。 如果想让发布的MIDlet套件具备自动升级的功能,那么需要提供一个服务器端程序,服务器端程序能够检测是不是有更新版本的应用程序下载,并且可以将这个结果告诉给客户端。通常这样的服务器端可以实现为Web应用程序,用servlet和MIDlet通信。当用户运行MIDlet的时候,MIDlet首先连接指定的服务器获取信息,如果没有新版软件则正常运行,如果有新版本的软件则是用platformRequest()方法请求安装应用程序。 if (update == NEED_UPDATE) { try { platformRequest("http://www.j2medev.com/wap/autoupdate.jar"); destroyApp(true); notifyDestroyed(); } catch (ConnectionNotFoundException ex) { ex.printStackTrace(); }
}else{ //do something else } 这里我们在代码中执行destroyApp(true),让MIDlet套件主动退出。如果想做的更为专业,可以在检测到有新版本的软件的时候,显示一个对话框让用户选择是否自动更新,根据用户的选择执行相关的操作。下面的代码UpdateMIDlet可以用于演示此项功能。 /** * author mingJava * Created on 2006-3-8 */ package com.j2medev.autoupdate;
import java.util.Random; import javax.microedition.io.ConnectionNotFoundException; import javax.microedition.lcdui.Display; import javax.microedition.lcdui.Form; import javax.microedition.midlet.MIDlet; import javax.microedition.midlet.MIDletStateChangeException;
public class UpdateMIDlet extends MIDlet {
public static int NEED_UPDATE = 0;
public static int NO_UPDATE = 1;
private Display display = null;
protected void startApp() throws MIDletStateChangeException { if (display == null) { display = Display.getDisplay(this); int update = (new Random().nextInt() >>> 1) % 2; if (update == NEED_UPDATE) { try { platformRequest("http://www.j2medev.com/wap/autoupdate.jar"); destroyApp(true); notifyDestroyed(); } catch (ConnectionNotFoundException ex) { ex.printStackTrace(); }
}else if(update == NO_UPDATE){ Form form = new Form("Test"); form.append("No update is needed"); display.setCurrent(form); } } }
protected void pauseApp() {
}
protected void destroyApp(boolean arg0) throws MIDletStateChangeException {
}
} 为了模拟是否有升级软件的可能,我们在startApp()中随机生成一个随机数update。如果update等于0则代表有升级版本,如果update等于1则代表不需要升级。为了简单起见,这里我们没有编写联网检测升级版本的程序。此程序在Nokia 7610上测试通过,可以自动升级MIDlet套件。 如何配置wtk才能使用platformRequest这个要手机支持,自己应该无法配置. 在模拟器下如果要访问网页,需要修改lib目录下的system.config文件,添加 com.sun.midp.midlet.platformRequestCommand:"D:\Program Files\Maxthon\Maxthon.exe"" 这个D:\Program Files\Maxthon\Maxthon.exe是浏览器所在目录. 在J2ME和WAP中实现电话呼叫功能本文介绍如何在J2ME平台和WAP页面实现电话呼叫的功能。关于WAP和J2ME的区别并不在讨论范畴,读者可以自行查阅资料。 MIDP 1.0中没有提供电话呼叫的功能。在MIDP 2.0中,javax.microedition.midlet.MIDlet类提供了platformRequest()方法来请求设备来完成特定的请求,请求的内容由platformRequest()方法的String类型的参数url来标识。可能是调用浏览器打开某个特定的wap页面,也可以是呼叫电话。在设备资源可用的时候,系统会把MIDlet放在后台执行,而把特定的应用程序,例如电话程序或者浏览器放在前台来执行。呼叫电话的时候可以使用如下的形式: try{ this.platformRequest("tel:13810000000"); }catch(ConnectionNotFoundException ex){ ex.printStackTrace(); } 这里提供了一个简单的例子,在Nokia 7610(支持MIDP 2.0)上测试通过,成功呼叫了指定的电话。 /* * TeleMIDlet.java * * Created on 2005年12月15日, 下午9:56 */ package com.j2medev.call; import javax.microedition.io.ConnectionNotFoundException; import javax.microedition.midlet.*; import javax.microedition.lcdui.*; /** * * @author Administrator * @version */ public class TeleMIDlet extends MIDlet implements CommandListener { private Display display = null; private List main = null; public void startApp() { if(display == null){ display = Display.getDisplay(this); main = new List("测试电话功能", List.IMPLICIT); main.append("13810000000", null);//这里的电话号码是虚拟的 main.append("13810000001", null);//这里的电话号码是虚拟的 main.setCommandListener(this); } display.setCurrent(main); } public void pauseApp() { } public void destroyApp(boolean unconditional) { } public void commandAction(Command cmd,Displayable displayable){ if(cmd == List.SELECT_COMMAND){ String number = main.getString(main.getSelectedIndex()); try{ this.platformRequest("tel:"+number); }catch(ConnectionNotFoundException ex){ ex.printStackTrace(); } } } } 相比J2ME技术,WAP更类似于Web,是一种服务器端为主的技术。在WML中可以调用设备的WTAI函数来呼叫特定的电话号码,代码如下所示: <input name="phone_no" format="*m" value="13"/> <do type="option" label="呼出号"> <go href="wtai://wp/mc;$(phone_no)"/> </do><br/> 或者直接写入电话号码的方式: <a href="wtai://wp/mc;1331597312*">拨打电话</a> 在MIDP 2.0和WAP平台中都对电话呼叫提供了支持,本文进行了简单的总结,希望对您的实际项目有所帮助。 模拟器实现触摸屏修改C:\WTK25\wtklib\devices\DefaultColorPhone\DefaultColorPhone.properties 下的touch_screen=true How to create thumbnail in Java MECreate a thumbnail image from a large image or scale an image in Java ME
private Image createThumbnail(Image image) { int sourceWidth = image.getWidth(); int sourceHeight = image.getHeight(); int thumbWidth = 64; int thumbHeight = -1; if (thumbHeight == -1) thumbHeight = thumbWidth * sourceHeight / sourceWidth; Image thumb = Image.createImage(thumbWidth, thumbHeight); Graphics g = thumb.getGraphics(); for (int y = 0; y < thumbHeight; y++) { for (int x = 0; x < thumbWidth; x++) { g.setClip(x, y, 1, 1); int dx = x * sourceWidth / thumbWidth; int dy = y * sourceHeight / thumbHeight; g.drawImage(image, x - dx, y - dy, Graphics.LEFT | Graphics.TOP); } } Image immutableThumb = Image.createImage(thumb); return immutableThumb; } You may also use http://www.java-tips.org/java-me-tips/midp/how-to-implement-oom-in-and-oom-out.html If you are using S60 devices, you can load the thumbnails generated automatically by the Gallery application. They are located in the "_PAlbTN" sub-folders of Image. For more details on this, check Thumbnail_path_for_3rd_edition_devices. 如何在Java ME平台上获取手机串号注意:在Java ME平台上获取手机串号,在某些手机中是可行的,但是某些手机中就是无法获取的。对于即便是可以获取串号的手机来说,也仅仅限于进行了签名的应用程序才可以获得手机串号。
在诺基亚手机中,我们可以这样获取手机串号: System.getProperty("com.nokia.mid.imei") 在Series60平台的手几种,这样的获取串号操作需要你的应用程序被认证为网络运营商信任的应用程序或者手机生产商信任的应用程序,并且只有在Series40第三版功能包1之后的手机设备中才能够使用。 On Series 40 phones this requires that your midlet is signed to either operator or manufacturer domain, and this is only available Series 40 3rd Edition FP1 devices and newer. 在S60平台,有些S60第三版的手机已经实现了这样的功能,如E60,E61,E62,但是不包括3250和5500。对于S60平台,获取串号并不需要签名认证。 在西门子手机中,我们可以这样获取传号: System.getProperty("com.siemens.IMEI") 其他手机: 三星: System.getProperty("com.samsung.imei");
索尼爱立信: System.getProperty("com.sonyericsson.imei");
摩托罗拉: System.getProperty("IMEI" );
诺基亚: System.getProperty("phone.imei");
System.getProperty("com.nokia.IMEI");
j2me调用系统程序方法:platformRequesttry { MIDlet.platformRequest方法可以支持很多的应用调用(如tel、fax、http访问无线网络等)或者应用的安装,但是具体的使用情况要看具体的手机提供商是否在手机上提供此类的功能了。 详细的解释: 一、调用过程: 调用platformRequest方法后,用指定的URL表示的请求被发给了设备: 情况1: 情况2: 二、方法调用的影响: platformRequest是一种非阻塞的方法,所以该方法不能够将多个请求进行排队。在请求被处理前MIDlet suite必须退出的平台只能处理最后的一个请求。而在MIDlet suite和请求能够同时处理的平台,每个发自MIDlet suite的请求必须被实时的传送给平台软件来处理。 如果URL所描述的请求引用了一个MIDlet suite(无论是一个应用程序描述还是一个jar文件),处理这个请求的应用将解释URL,将其作为安装某个package的请求。在这种情况下,平台将进行一般MIDlet suite安装,用户也将控制这个安装过程(包括取消安装、下载、确认安装)。如果被安装的MIDlet suite是当前运行的MIDlet suite的更新版本,则在执行升级之前,平台将停止当前运行的低版本MIDlet suite。有一些平台,在安装进行之时当前运行的MIDlet suite可能需要停止。 如果URL描述的内容是一种tel:<number>的方式,这种方式在RFC2806中有所描述。平台必须解释这个URL将它作为初始化语音呼叫的请求。如果平台提供“电话”程序,则这个请求必须被传送到“电话”应用程序去处理。如果平台提供此功能,则必须具备能够建立本地和全局电话呼叫的能力,并执行DTMF(电话管理功能)传送拨号。但,并不是所有的RFC2806定义的URL格式都被实现,尤其是区号或者电话终端需要的内容。isdn子网,服务提供商和扩展特性可能被忽略掉。 而且设备可以选择性的支持不在上面列表中的URL的附加定义。 platformRequest方法可能会对用户的经济造成影响,比如使用无线网络、或者语音呼叫。所以在程序执行之前,平台必须获得用户的允许。这个获得用户允许的实现方式是比较自由的。例如在一些平台上每次询问用户时出现一个对话框。而另外一些平台可能执行相应的应用程序,弹出URL或者电话号码的输入字框并保持静止等待状态,直到用户点击装载或者拨号按钮后才执行。 January 29 SD和MMC记忆卡介面技术
January 26 在MIDP2.0中操作图片像素 转载 我们知道,在MIDP1.0中,除非我们利用特定厂商的API(比如Nokia),我们是没法对图片的像素进行操作的,但是在MIDP2.0中,Image和Graphics的功能都大大增强了。比如,我们可以获取Image的所有像素值,然后利用程序来修改这些像素(比如说ARGB各自的值),最后再把修改后的像素图绘制出来。通过直接操作图片像素,我们就获得了一种很强大的能力,用编程的方式实现出很多有趣的效果来,而不用额外制作新图片。比如说透明度渐变,颜色反转等。下面就是2个例子,分别实现透明度渐变和颜色反转的功能。 例题一: 透明度渐变效果的实现 给定一张图片,假如我们想实现这么一种效果:图片由全透明状态逐渐清晰,最后达到正常状态。要实现这一个过程,我们首先要获取该图片的所有像素值,逐步让这些像素的alpha值从0转变到正常,每改变图片的所有像素值一次,我们就请求刷屏一次,把最新的像素图画出来,这样我们就能实现透明度渐变的效果了。代码实现如下:
透明度渐变效果如下: 例题二:颜色反转 在手机游戏中,我们经常会碰上这样一种情况,比如我方飞机和敌方飞机外观是完全一样的,唯一的区别就是颜色不同,比如说敌方飞机是红色的,而我方飞机是绿色的。在MIDP1.0中,我们就只好制作2张图片来表示2种飞机,自然,这样会造成jar空间的极大浪费。但是在MIDP2.0中,通过对图片直接进行像素操作,反转RGB中的一个值,我们只需要一张图片就够了,样例代码如下: ColorMIDlet.java
安全高效的使用PNG图 转载 众所周知,J2ME程序使用的最多的图片格式为PNG格式,如何在程序中使用PNG图片对于程序来说有很多的技巧,我将以我的一些经验技巧来讨论有关PNG图片在程序中的应用。 通常情况,一个项目开始的时候策划出了需求,美工出了图片,程序员的代码也开始写了,程序员需要图片时,美工给的图片都为一张张静态的图片,然后通过引擎(或者一些工具)导成程序需要的动画序列,和图片数组,程序在Canvas中把图片数组按照图片序列标志的顺序、位置、桢数表现出来。动画是组成游戏的非常重要的部分。因而图片资源的大小、存储方式等对程序生成的jar文件的大小和耗费内存的多少有非常大的影响。在forum.nokia.com和J2ME WTK2.2的一些文档中我们可以看到一些关于图片资源如何优化的例子,在此我不予详述,但是提及,重点讲述我们的项目经验。 在一些文档中建议我们把所有的资源都放在一张足够大的PNG图片里面,我们对图片进行分割,这样做有非常大的好处,但是有一些缺点,比如我们把一张大图片读入我们的程序里面的时候,我们在菜单部分仅仅需要和菜单那部分资源,不需要其他的资源,这样我们读出的部分显得就非常的浪费内存,我们可以采取把各种图片资源分别存放到几个大图片中,这样我们需要的时候把需要的部分从jar中读到内存,不需要的时候释放出去,这样可以保证一些运算内存比较小的设备使用很多图片资源,不会发生out of memory的异常或者错误。举个例子,一个游戏有菜单、玩游戏、排行榜这样三个部分,完全可以把图片分成三组存储,和菜单相关的存储为menu.png,游戏中的存储为game.png,排行榜需要的图片存储为range.png,我们进入菜单状态只读区menu.png这样程序浪费的内存相当少,进入游戏时先释放掉menu.png占用的资源,再调入game.png。在项目中这是非常好的应用实例。 在我们的项目中,有时不需要使用切割图像,我们乐于使用一些大小一样的矩形方块状的图片(一些小的公司没有良好的引擎设计时,一般采用这种方式,一些大公司有专门做引擎的e,所以一般采用上面的方法且优化了上面的办法)。因为一些压缩算法、和图片存储格式等众多原因造成了如下状况:把很多png图形放到一张png图片里面省更多的空间。我们如何省空间呢?答案是:自己设计一个资源读取器,把需要的所有png图片读取成2进制码并且按照我们能够简单使用的格式写成一个二进制文件,我们只需要在程序中读取这个二进制文件,在把里面的png还原出来即可。我在我们的项目中发现,单独使用21张14*14的png图像,与巴这21张png重新写成一个二进制文件(不采用任何压缩算法),后者比前者在jar中节省了10KB。所以说,我们在做游戏的时候,如果没有非常好的引擎,可以采用我们办法来节省空间——把松散的图片用程序写成一个二进制文件,在j2me程序中把这些资源读取出来。 综上,我叙述了两种不同的节省资源的方法,前一种需要比较强大的引擎支持,后一种则不需要,但是后一种确实节省的不如前一种多,但比单纯的用好多png图要节省的多而且不需要复杂的引擎。 January 25 正则表达式简记想必很多人都对正则表达式都头疼。今天,我以我的认识,加上网上一些文章,希望用常人都可以理解的表达方式来和大家分享学习经验。 开篇,还是得说说 ^ 和 $ 他们是分别用来匹配字符串的开始和结束,以下分别举例说明: "^The": 开头一定要有"The"字符串; "of despair$": 结尾一定要有"of despair" 的字符串; 那么, "^abc$": 就是要求以abc开头和以abc结尾的字符串,实际上是只有abc匹配。 "notice": 匹配包含notice的字符串。 你可以看见如果你没有用我们提到的两个字符(最后一个例子),就是说 模式(正则表达式) 可以出现在被检验字符串的任何地方,你没有把他锁定到两边。 接着,说说 '*', '+',和 '?', 他们用来表示一个字符可以出现的次数或者顺序。 他们分别表示: "zero or more"相当于{0,}, "one or more"相当于{1,}, "zero or one."相当于{0,1}, 这里是一些例子: "ab*": 和ab{0,}同义,匹配以a开头,后面可以接0个或者N个b组成的字符串("a", "ab", "abbb", 等); "ab+": 和ab{1,}同义,同上条一样,但最少要有一个b存在 ("ab", "abbb", 等。); "ab?":和ab{0,1}同义,可以没有或者只有一个b; "a?b+$": 匹配以一个或者0个a再加上一个以上的b结尾的字符串。 要点, '*', '+',和 '?'只管它前面那个字符。 你也可以在大括号里面限制字符出现的个数,比如 "ab{2}": 要求a后面一定要跟两个b(一个也不能少)("abb"); "ab{2,}": 要求a后面一定要有两个或者两个以上b(如"abb", "abbbb", 等。); "ab{3,5}": 要求a后面可以有2-5个b("abbb", "abbbb", or "abbbbb")。 现在我们把一定几个字符放到小括号里,比如: "a(bc)*": 匹配 a 后面跟0个或者一个"bc"; "a(bc){1,5}": 一个到5个 "bc." 还有一个字符 '│', 相当于OR 操作: "hi│hello": 匹配含有"hi" 或者 "hello" 的 字符串; "(b│cd)ef": 匹配含有 "bef" 或者 "cdef"的字符串; "(a│b)*c": 匹配含有这样多个(包括0个)a或b,后面跟一个c的字符串; 一个点('.')可以代表所有的单一字符,不包括"\n" 如果,要匹配包括"\n"在内的所有单个字符,怎么办? 对了,用'[\n.]'这种模式。 "a.[0-9]": 一个a加一个字符再加一个0到9的数字 "^.{3}$": 三个任意字符结尾 . 中括号括住的内容只匹配一个单一的字符 "[ab]": 匹配单个的 a 或者 b ( 和 "a│b" 一样); 想必很多人都对正则表达式都头疼。今天,我以我的认识,加上网上一些文章,希望用常人都可以理解的表达方式来和大家分享学习经验。 开篇,还是得说说 ^ 和 $ 他们是分别用来匹配字符串的开始和结束,以下分别举例说明: "^The": 开头一定要有"The"字符串; "of despair$": 结尾一定要有"of despair" 的字符串; 那么, "^abc$": 就是要求以abc开头和以abc结尾的字符串,实际上是只有abc匹配。 "notice": 匹配包含notice的字符串。 你可以看见如果你没有用我们提到的两个字符(最后一个例子),就是说 模式(正则表达式) 可以出现在被检验字符串的任何地方,你没有把他锁定到两边。 接着,说说 '*', '+',和 '?', 他们用来表示一个字符可以出现的次数或者顺序。 他们分别表示: "zero or more"相当于{0,}, "one or more"相当于{1,}, "zero or one."相当于{0,1}, 这里是一些例子: "ab*": 和ab{0,}同义,匹配以a开头,后面可以接0个或者N个b组成的字符串("a", "ab", "abbb", 等); "ab+": 和ab{1,}同义,同上条一样,但最少要有一个b存在 ("ab", "abbb", 等。); "ab?":和ab{0,1}同义,可以没有或者只有一个b; "a?b+$": 匹配以一个或者0个a再加上一个以上的b结尾的字符串。 要点, '*', '+',和 '?'只管它前面那个字符。 你也可以在大括号里面限制字符出现的个数,比如 "ab{2}": 要求a后面一定要跟两个b(一个也不能少)("abb"); "ab{2,}": 要求a后面一定要有两个或者两个以上b(如"abb", "abbbb", 等。); "ab{3,5}": 要求a后面可以有2-5个b("abbb", "abbbb", or "abbbbb")。 现在我们把一定几个字符放到小括号里,比如: "a(bc)*": 匹配 a 后面跟0个或者一个"bc"; "a(bc){1,5}": 一个到5个 "bc." 还有一个字符 '│', 相当于OR 操作: "hi│hello": 匹配含有"hi" 或者 "hello" 的 字符串; "(b│cd)ef": 匹配含有 "bef" 或者 "cdef"的字符串; "(a│b)*c": 匹配含有这样多个(包括0个)a或b,后面跟一个c的字符串; 一个点('.')可以代表所有的单一字符,不包括"\n" 如果,要匹配包括"\n"在内的所有单个字符,怎么办? 对了,用'[\n.]'这种模式。 "a.[0-9]": 一个a加一个字符再加一个0到9的数字 "^.{3}$": 三个任意字符结尾 . 中括号括住的内容只匹配一个单一的字符 "[ab]": 匹配单个的 a 或者 b ( 和 "a│b" 一样); 现在, 用户名的开始和结束都不能是句点。 服务器也是这样。 还有你不能有两个连续的句点他们之间至少存在一个字符,好现在我们来看一下怎么为用户名写一个匹配模式: ^[_a-zA-Z0-9-]+$ 现在还不能允许句号的存在。 我们把它加上: ^[_a-zA-Z0-9-]+(\.[_a-zA-Z0-9-]+)*$ 上面的意思就是说: "以至少一个规范字符(除了。)开头,后面跟着0个或者多个以点开始的字符串。" 简单化一点, 我们可以用 eregi()取代 ereg()。eregi()对大小写不敏感, 我们就不需要指定两个范围 "a-z" 和 "A-Z" ? 只需要指定一个就可以了: ^[_a-z0-9-]+(\.[_a-z0-9-]+)*$ 后面的服务器名字也是一样,但要去掉下划线: ^[a-z0-9-]+(\.[a-z0-9-]+)*$ 好。 现在只需要用“@”把两部分连接: ^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*$ 这就是完整的email认证匹配模式了,只需要调用 eregi(‘^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*$ ’,$eamil) 就可以得到是否为email了。 正则表达式的其他用法 提取字符串 ereg() and eregi() 有一个特性是允许用户通过正则表达式去提取字符串的一部分(具体用法你可以阅读手册)。 比如说,我们想从 path/URL 提取文件名 ? 下面的代码就是你需要: ereg("([^\\/]*)$", $pathOrUrl, $regs); echo $regs[1]; 高级的代换 ereg_replace() 和 eregi_replace()也是非常有用的: 假如我们想把所有的间隔负号都替换成逗号: ereg_replace("[ \n\r\t]+", ",", trim($str)); 最后,我把另一串检查EMAIL的正则表达式让看文章的你来分析一下。 "^[-!#$%&\'*+\\./0-9=?A-Z^_`a-z{|}~]+'.'@'.'[-!#$%&\'*+\\/0-9=?A-Z^_`a-z{|}~]+\.'.'[-!#$%&\'*+\\./0-9=?A-Z^_`a-z{|}~]+$" 如果能方便的读懂,那这篇文章的目的就达到了。 为基于J2ME的手机开发移动3D游戏之保留模式 一、简述 既然现在你已对3D API比较熟悉并了解了3D图形是如何加入到移动Java应用程序中的。下面将继续告诉你怎样使用3D造型软件以使编码和设计更为简单。 如今,3D图形几乎是任何一部游戏的关键部分,甚至一些应用程序也通过用3D形式来描述信息而获得了成功。如前文中所述,以立即模式和手工编码建立所有的3D对象的方式进行开发速度很慢且很复杂。应用程序中多边形的所有角点必须在数组中独立编码。在JSR 184中,这称为立即模式。 另外一种更高级的模式称为保留模式,它允许设计者使用诸如3D Max Studio等3D建模软件来设计场景图,然后把它们应用在程序中。 二、3D编辑器 现在,最流行的商业动画制作软件应是3D Studio Max,它支持输出模型或场景图到M3G格式(JSR 184中指定的文件格式)。该文件格式是专门制订的,以适用于移动设备的特有需要。然而,3D Studio Max非常昂贵,即使它是一个很好的工具,也可能并不适合于任何一个人。 Superscape公司有他自己的Swerve产品家族(Swerve Studio,Swerve Client,Swerve Content),以帮助软件开发者来开发基于3D Java的本机应用程序。遗憾的是,Swerve Studio仅适于有限数目的对Superscape非常熟悉的开发者。 还有一个自由工具可以选择使用:Blender。Blender是一个开源的3D造型工具,其实它的功能相当强大。你可以用Blender来进行任何3D设计-从简单的造型到完整的动画制作。尽管现在还没有输出工具来输出Blender模型到M3G文件中,但是可能很快就出现一些可用的工具(因为Blender是开源的)。 三、建模 如何在MIDP应用程序中使用M3G 文件呢?首先,你需要一个已有某种3D模型的M3G文件。你可以用Google引擎快速查找一下,也可以使用和WirelessToolkit 2.2(在Demo3D 文件夹下)开发包一起发布的现成文件。在本文中,我们将对Sun的Pogoroo例程(编者注:Sun开发工具包自带例程)作深度修改(简化)。我们不让它动起来或者做任何奇特的事情,而仅仅在屏幕上展示各个对象。 四、加载World 首先,要从M3D文件中加载World。在pogoroo.m3g文件中,你会看到一只袋鼠在一根弹簧单高跷杆上跳跃,其身边是一片绿茵。下面的列表1调用了加载器类的方法load()。 列表1. 加载 try { //从M3D文件中加载World myWorld = (World)Loader.load("/pogoroo.m3g")[0]; getObjects(); setupAspectRatio(); } catch(Exception e) { e.printStackTrace(); } 五、从3D世界中取得对象 3D世界已经被加载,现在你必须从中取得各个对象(见列表2)。这里,3D世界中有四个对象,其中之一是有关动画(袋鼠在单脚跳)的信息。你可以使用World的find()方法来取得这些对象。 列表2. 从3D World中取得对象 try { tRoo = (Group) myWorld.find(POGOROO); tCams = (Group) myWorld.find(CAMERA); acRoo = (Group) myWorld.find(TRANSFORM); animRoo = (AnimationController) myWorld.find(ROO); //取得动画的长度 AnimationTrack track = acRoo.getAnimationTrack(0); animLength = 1000; // 缺省长度为1秒 if (track != null) { KeyframeSequence ks = track.getKeyframeSequence(); if (ks != null) animLength = ks.getDuration(); } } catch(Exception e) { e.printStackTrace(); } 六、设置窗口宽高比例 你必须设置窗口的宽高比例以使对象能够正确着色。列表3中的代码是未改动的-基本上同Sun的例子一样。首先,检查画布的宽度和高度,然后根据相机的类型来计算宽高比例。 列表3. 设置宽高比例 void setupAspectRatio() { viewport_x = 0; viewport_y = 0; viewport_width = myCanvas.getWidth(); viewport_height = myCanvas.getHeight(); Camera cam = myWorld.getActiveCamera(); float[] params = new float[4]; int type = cam.getProjection(params); if(type != Camera.GENERIC) { //计算窗口的宽高比 float waspect=viewport_width/viewport_height; if (waspect float height = viewport_width/params[1]; viewport_height=(int)height; viewport_y=(myCanvas.getHeight()-viewport_height)/2; } else { float width = viewport_height*params[1]; viewport_width=(int)width; viewport_x=(myCanvas.getWidth()-viewport_width)/2; } } } 七、刷新视图 为了刷新视图,你可以用TimerTask来调用画布的repaint()方法。另一种方法是直接使用线程,然后创建ExampleCanvas(画布类的名字)来实现Runnable接口。 列表4. 刷新视图 private class RefreshTask extends TimerTask { public void run(){ if(myCanvas != null && myGraphics3D != null && myWorld != null) { int startTime = (int)System.currentTimeMillis(); int validity = myWorld.animate(startTime); myCanvas.repaint(viewport_x, viewport_y, viewport_width, viewport_height); } } } 为J2ME开发移动3D游戏之立即模式 一、简述 现在,移动游戏和移动应用开发极为热门!游戏中需要有时髦漂亮的图形,其设计标准比以前任何时候都要高。本文将告诉你怎样用酷毙的移动3D图形API为J2ME设备开发3D图形游戏。 如果你在用MIDP1.0进行用户接口编程,那么有两条路你可以选择:使用高级的UI类或者一切由你自己从头开始。作为游戏开发者,第一种选择往往是不可能的;这是为什么游戏开发者不得不为他们的高级游戏开发自己的3D引擎的原因。无疑,这需要付出大量的时间和努力,而缺乏浮点数支持的CLDC 1.0(MIDP 1.0正是建于其上)对问题的解决没有多大帮助。 在MIDP 2.0中,有一个可选的叫移动3D图形API的软件包,或者叫JSR 184。该API是第一个基于Java标准开发的移动设备上的三维图形软件包。该API既有高级又有低级图形特征;其中,高级特征称为保留模式,低级特征称为立即模式。保留模式使得开发者有可能使用场景图形并使场景中的物体根据虚拟相机和灯光的位置进行自身的着色。立即模式能够允许应用程序直接进行物体绘制。如果需要,可以在同一个应用程序中使用这两种模式。 本文着重介绍立即模式。 二、3D API 让我们以列举和解释该3D API中的类作为开始。除了这些API外,JSR 184还包含了一个场景图形结构和一个相应的文件格式以有效地管理和配置3D内容。该文件格式定义了一种m3g文件,这种文件典型地从3D建模文件应用程序中转换而来。 表1.3D API类
我们将开发一个简单的旋转一个多边形的3D应用程序为例。该多边形是一个立方体,它的纹理是一张旧汽车相片。列表1展示了例程midlet的主要类-应用程序的中心类。该类负责创建应用程序并建立起运行MyCanvas的计时器。 列表1. MIDletMain类
列表2显示了MyCanvas类,该类包含了应用程序的所有图形逻辑。init()方法负责结点的创建,纹理文件的装载并设置纹理,外观和背景也被一起设置。paint()方法负责着色并旋转立方体。图1展示了正在一个模拟器中运行的实际程序。 列表2. MyCanvas类
谈谈国内手机游戏制作和国外的比较 (转载)谈谈国内手机游戏制作和国外的比较 adaken 我做手游已经3、4个年头了,经历过行业里的风风雨雨、分分合合……其他的这次先不说,单纯从产品质量的角度来看,我认为,国内手游的整体水平 (这里单指Kjava、Brew等视频游戏)这几年虽然不至于倒退,但是说实话,真没有什么太大的提高。反观目前世界上一批领先的手机游戏开发商,他们的产品却越做越好,公司也越做越大。 看到这里,可能很多人都会马上提出驳论。没关系,请继续看完再发表:) 因为是随笔,所以我把能想到的都罗列出来,供大家探讨。 1、产品创意 谈到创意,说实话,我觉得这可能是我们和老外们差距最小的一点。因为目前各种信息都共享了,游戏行业的东西更是如此,每天都会有很多新东西塞进我们的大脑。再加上我们有些负责创意、策划的同志们,凭着对各种平台游戏的了如指掌,稍微动动脑子mix一把,就弄出了不少很有意思的点子……但遗憾的是,很多这样的游戏并没有变成产品,变成产品的那些一半以上没有上线,上线的那些一半因为制作中的某些环节,也已经面目全非,最初的那点创意根本没有体现出来!~所以我总在对自己说,国内现在不缺(或者说‘暂不需要’)好的创意。希望做游戏策划的同志们不要把主要精力放在这里了。 2、游戏策划 游戏设计文档,是一款游戏产品进行开发的根本依据。也是一名策划最应该花心思的地方。……不可否认,我们的策划大部分都没有受过正规的培训(不是不想,而是找不到),而且入行时间大都很短,没有丰富的开发经验,等等原因吧……所以,造成的现状是,设计文档写得乱七八糟、主次不分、避重就轻。(说到这里,我本想举几个事例,但是这样会把文章拉的很长,还是放弃了,但我会在以后专门就这个问题写些具体的东西。)我曾见过某外国公司撰写一款掌机游戏设计文档,长达190多页。里面并没有长篇大论地分析当前市场形势、如何宣传推广,对物品、道具等内容也是简洁明了的描述,甚至只写了前面的几个,后面再进行补充。重点全部是游戏中的各种交互和规则,细致到连游戏中的对话哪里需要空格,那里换行都有说明……相反,我也看到国内一些公司的设计文档,说实话,要是不告诉我这是设计文档,我还以为是枪手们写的游戏评论呢!~洋洋洒洒、夸夸其谈,大半的篇幅用来告诉读者这个游戏哪里好玩、哪里创新。我就奇怪了,程序们看到这样的东西,居然还真能开发出产品来,在此~给国内踏实肯干的程序员们鞠个躬。总结:要想高效地开发出好产品,必须要把设计文档写好! 3、游戏美术 对于我们手游的美术水准,我认为是:全球中等水平上的参差不起。主要原因是:国内的开发商对美术环节往往不重视,导致美术人员的工作情绪普遍比较失落,没有进行创新、再学习的意愿……像一部印刷机一样,不断地揣摩、翻译策划人员心目中的游戏景象,而没有主动性的创作。不过,我认为,‘美术制作’是整个游戏开发过程中最容易快速提高的环节,只要制作公司的高层意识到这一点,再经过有效地培训,应该会有一个比较好的改观。但是,我不希望看到,假如有一天,国外优秀的手机游戏大量涌进进入我们的市场,当用户们的选择告诉我们‘该重视游戏画面了’的时候,再意识到这些就已经晚了…… 4、程序开发 看看市场上那些取得成功的国产网络游戏,就应该明白,我们并不缺少优秀的程序员。但在手游行业里,我认为,这些程序员的心态出了问题,主观或客观的原因,让他们不能全身心地投入开发。整个行业的急功近利、生产周期的不断压缩、工作中的交流不畅……等等等等。因为我本人没有直接和国外的程序员打过交道,对他们的工作方法等内容也知之甚少,所以不敢在此妄言什么。只是有一点,让我想了很久:国外很多优秀游戏的策划或是制作人本身也是程序员,或者是程序员出身,这种情况在国内还是不多的。 5、整体包装 游戏的Demo出来了,开发团队内部欣喜若狂,一边忙着测试和移植,一边联系合作伙伴准备申报吧!……直到游戏全部完工,甚至游戏顺利上线,除了那几张粗糙的截图和几百字牵强的评论,玩家似乎看不到关于这款游戏更全面、更深入、更吸引人的东西了……这就好比我们准备在超市购买一瓶饮料,在这之前如果你根本不清楚这个瓶子里装的液体是什么颜色、什么味道、也没有任何的介绍和比较诱人的宣传包装,你会毫不犹豫地掏钱买下它么?……至少我不会,更何况,现在一款游戏的价格绝对比一瓶饮料的贵出很多。我要说的是,一款游戏的完成,不仅是jad、jar、几张截图、几份文档的完成。而是需要全方位的包装,比如:网站上的游戏专区、精美的宣传图片、与主流媒体合作推广……等等。说得简单一点,看看gameloft、mr.goodliving、com2us人家的网站吧,一种强烈的、对那些游戏的获取感会油然而生! ~这些其实是很表面的东西,但我们早晚都要做的,行业的快速发展势必造成优胜劣汰,早点学会并做到,就会让自己在发展中处于主动。 6、…… 其实想写的东西还有很多,但从一名游戏策划的角度来看,写到这里就足够了。当然,欢迎朋友们继续补充和交流。 结尾,真心的希望我们国内的游戏产业、手机游戏产业,能迅速发展壮大,做出一些能够‘感动’玩家的精品。 来源:电玩俱乐部 J2ME内存占用详解及优化方法 在S40上经常出现OutOfMemoryError异常。
首先了解一下分析内存占用的方法,一般有两种:模拟器自带工具和Runtime类方法。
模拟器自带工具:WTK带了一个Memory Monitor,首先运行他你的程序会慢的一塌糊涂,这对游戏开发者来说简直是无法忍受的。而且根本无法显示正确的内存占用量,所以一般使用的是7210模拟器自带的内存监视器,模拟的很准,但唯一的缺点是内存太少,才200K。也见过某些人使用3220的模拟器监视内存,好像内存稍微大一点,我还没来得及尝试就再也不用为老40写程序了,庆幸。 Runtime类方法:经常用这个语句System.out.println(Runtime.getRuntime().freeMemory());后来集成进了我的引擎,他能够显示当前剩余内存。不记得有多少次我用它在老40上来寻找内存占用峰值。 了解了分析内存的方法,来看看内存占用的罪魁祸首:程序和资源。 程序:类会被编译成class字节码文件随MIDlet的启动加载进内存,而且是一次性全部加入。也就是说MIDlet里类个数越多、单个类程序越长、类内字符串常量及数据越多,编译后的class文件就越大,载入后占用的内存也越多。我经常在MIDlet类的构造函数里用Runtime方法来查看MIDlet启动后整个程序占用内存量。 优化方法: 1.某些同志将MIDlet程序写成两个类来减少内存占用量,但是以牺牲Java的OOP特性为代价的。在程序比较大时这种弊端将尤为显见。而且CoCoMo曾经遇到过单个类过大,载入时间过长而违反百宝箱有关Logo 6秒时间限制的情形。因而我现在的程序加带引擎一般都是6-7个类。 2.尽量编写优雅的代码,减少函数数量,在程序发布时去掉try catch,最大限度的减少程序行数,这一般都是在老40上没有办法的办法,现在CoCoMo已经不靠这个来省内存了。 3.将数据及字符串写进文件,在用时方载入内存,不用时设为null。 4.I/O操作getClass().getResourceAsStream(file);、数据库操做RecordStore.openRecordStore(name, true);、声音创建Manager.createPlayer();、图像创建Image.createImage(file);会在短时间内占用大量内存且过后释放,如果MIDlet程序内存剩余量不足则会在这些函数频繁调用时发生内存溢出,产生所谓的内存峰值,尤其在老40上比较普遍。当你再次与讨厌的OutOfMemoryError碰面时,多用Runtime查找内存峰值发生位置并尽量将这些语句分开调用,并灵活运用System.gc()来及时回收。 资源: 图片:是占用内存的大户,尤其是手机游戏图片资源众多。对图片资源在内存中占用量的计算成为J2ME游戏开发者的经常性工作,CoCoMo来解释一下如何计算图片在内存中的占用量: 内存占用量=宽*高*像素字节数,其中像素字节数因机型而异。 例如一张64*64的图片在7210上的内存占用量=64*64*1.5=6144(字节)=6K、在S60上的内存占用量=64*64*2=8192(字节)=8K。像素字节数因机型而异,例如7210是4096色机型,也就是说用12位来表示一个像素,所以乘上1.5,而S60是65536色的机型,用16位来表示一个像素,所以乘上2。 优化方法: 有些人认为压缩图片可以节省内存,这种想法是错误的。根据上面的解释图片载入内存后只和宽高有关系,和图片数据量大小没有任何关系,压缩图片只能减少jar大小而不能减少内存占用量。 1.静态法:减小图片大小,宽高小了结果当然小了。根据这个思路出现了动画编辑器之类的工具,像gameloft的波斯王子,人物被分割后使人体的部位可以重用,各部位紧凑放置都是为了较少图片大小,充分利用图片中的每一寸空间。 2.动态法:减少同一时刻载入内存的图片数。CoCoMo曾经在火影武士项目中遇到过这种情况,当时有6种怪物,如果同时载入内存在老40上肯定爆掉了,但是每关只出现两到三种怪物,所以每一关只需要载入该关出现的怪物图片即可。现在想起来当时做这个项目在老40上溢出频出,真把我搞死了。 声音:声音也是比较耗用内存的资源,声音中音轨所占的byte会转化成字节流被载入到内存中。因而减少音轨所占byte即可减少内存耗用量。目前gameloft的做法是用声音转化工具将mid转化为ott,然后变为ByteArrayInputStream字节流来创建Player。 转载自CoCoMo的空间。 December 11 用AS消除位图锯齿通过AS动态载入的图片在缩放的时候出现模糊和锯齿,由于这些位图是通过AS从外部载入进来的,不能在Flash制作时通过修改库里图片的属性来消除锯齿。外部加载的通过bitmap来进行解决,主要用到MovieClip.attachBitmap中的一个smooth参数的设置,来进行平滑处理。代码如下: /*****进行消锯齿与不消锯齿的对比*****/
import flash.display.BitmapData; /*************加载******************/ var container1:MovieClip = createEmptyMovieClip("container1", getNextHighestDepth()); var container2:MovieClip = createEmptyMovieClip("container2", getNextHighestDepth()); var loader1:MovieClipLoader = new MovieClipLoader(); var loader2:MovieClipLoader = new MovieClipLoader(); var obj:Object = new Object(); obj.onLoadInit = function(target:MovieClip) { trace("onLoadInit"); loaderDispose(target); }; loader1.addListener(obj); loader1.loadClip("1.jpg", container1); loader2.addListener(obj); loader2.loadClip("1.jpg", container2); /**********图片处理***************/ var bitmapLoader1:MovieClip = createEmptyMovieClip("Bloader1", getNextHighestDepth()); var bitmapLoader2:MovieClip = createEmptyMovieClip("Bloader2", getNextHighestDepth()); var bitmap1:BitmapData; var bitmap2:BitmapData; function loaderDispose(mc:MovieClip) { mc._visible = false; /*******不平滑*******/ if (mc._name == "container1") { bitmap1 = new BitmapData(mc._width, mc._height, true, 0x00FFFFFF); bitmap1.draw(mc); bitmapLoader1.attachBitmap(bitmap1, 0); bitmapLoader1._x = 0; bitmapLoader1._xscale = bitmapLoader1._yscale=250; } else { /******平滑处理*******/ bitmap2 = new BitmapData(mc._width, mc._height, true, 0x00FFFFFF); bitmap2.draw(mc); bitmapLoader2.attachBitmap(bitmap2, 1, "auto", true); //主要就是这个smooth参数,true表示平滑 bitmapLoader1._x = 0; bitmapLoader2._x = 320; bitmapLoader2._xscale = bitmapLoader2._yscale=250; } } 效果如图: |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|