本文是基于Nokia Series 60和Symbian OS技术,指导开发者在现在的Series 60移动电话终端上开发高级的移动游戏。
Symbian
60平台v1.x。Series 60平台的基本操作系统。这适用于Series 6.1作为Series 注意:本文档中讨论的技术路线都是以Symbian>
Symbian操作系统简介
Symbian操作系统是所有Symbian OS电话共享的应用编程接口( API)技术的公共核心。 这个核心被命名为"通用技术"(generic technology,简称为GT),它被分成不同的版本。 GT包括一个多任务核心、通讯、数据管理和图表中间件,低级图形用户界面框架和应用程序引擎。
象智能电话这样的小型手持设备,通常是资源非常紧张的设备。 这种设备的尺寸和制造费用限制了可用的存储器,处理速度和电池寿命。 尽管以上这些资源缺乏,但是这些设备还是需要能够稳定运行相当长的一段时间,甚至数个月。 一旦出现资源溢出的错误,对于系统来说重要的事情就是返回到前一个稳定的状态,而不失去任何重要的数据。 这使得完全地捕捉和处理每个运行错误对于系统和应用程序来说是非常重要的。
由于资源溢出而出现的错误,和所有的运行期错误一样被称为异常。 在标准的C++中,这些异常是使用try-catch-throw机制处理的,但是因为它造成了代码长度的负面影响,所以Symbian OS提供了它自己的称为trap harness的机制。
Symbian开发他们自己的异常处理程序的另一个原因是开发Symbian OS的时候,try – catch – throw机制还不是C++标准的一部分。trap- harness的概念是使用一个TRAP宏把可能引发异常的函数封装起来。这个宏可用于捕获多个函数,并且这些函数可以嵌套。万一出现一个异常,导致异常的函数的执行就会通过User::Leave函数终止,相当于标准的C++异常处理中的throw。这被称为一个leave(离开),它将返回程序执行到结束的TRAP宏,在那里相应的恢复动作可以被执行。
Symbian OS还提供一个工具,以便清除异常。一个cleanupstack被用于引用只被一个自动变量引用,并且如果出现一个leave的时候需要被释放的对象。TRAP宏将销毁cleanupstack中自动变量分配的内存。
Symbian OS提供一个用于在单一线程之内非抢先式多任务处理技术的系统。这个包括活动对象和活动调度程序的系统的设计目标是降低运行时间成本和解决与优先计划线程冲突的同步问题。Symbian OS中的每个应用程序由一个活动调度程序和一个或多个活动对象组成。这个调度程序封装一个需要异步的服务和依照它们的优先权安排活动对象的等待循环。活动对象封装实际的异步服务。
需求
与许多其它用于游戏的设备不同,智能电话需要在游戏中或者任何其他的应用程序运行的时候能够通知用户各种系统事件。 应用程序需要考虑到可能的中断,例如因为打进来的电话或者消息,并且它们需要根据情况进行处理。 应用程序还应该不消耗设备资源,例如过度的使用内存或者电量。
通知用户的大多数系统消息使用系统自己的对话框,称为全局通知。 这个对话框具有比任何应用程序都要高的窗口优先权,因此它们出现在应用程序的前端。 在系统事件中,一个异常是打进来的电话造成电话通信应用程序变成最前端的应用程序而把被中断的应用程序留作背景。 然而,所有的系统端事件具有一个公共特征,可以被一个应用程序捕捉。 当一个系统事件发生时,最前端的应用程序失去焦点。 这造成应用程序用户接口类(CAknAppUI)的HandleForegroundEventL方法被调用。 通过覆盖这个方法,应用程序可以执行需要的动作,例如暂停正在进行的游戏。
应用程序需要注意电池使用量。 当一个电话在预定的时间内没有使用的情况下,它会进入睡眠方式以便消耗最少的电量。 如果一个应用程序持续做后台处理,例如在一个循环中查询一个变量,电话可能不会进入休眠状态。 所有的查询应该在程序块循环中执行,并且所有的计时器应该在游戏暂停的时候停止。 一旦一个计时器需要来维护和另一个终端的连接,这个计时器的频率应该降到最小水平。 当在预先设定的时间间隔里一直没有用户活动的时候,应用程序还可以从一个系统端计时器中取得事件。 要做到这些可以使用e32std.h头文件中可以找到的RTimer::Inactivity方法。 在电池供电的设备中,软件需要对突然断电有所准备。 电池可能会没电,或者用户可能把它从设备中拿出来。 如果重要的用户数据被编辑的时候,就更应该注意这些情况了。 每隔一段时间这些数据就应该被保存,并且能够在重新启动之后恢复。 此外,应用程序应该对被损坏的数据有所准备,并且能够安全地从这种情况下恢复。
限制
除了有限的存储量之外,智能电话和PC相比有其他几个限制。 首先,智能电话没有像PC一样高效的处理器。 智能电话基本都没有数学处理器,因此时间单位计算应该使用整数实现。 Symbian OS作为一种游戏平台还有一些约束。 例如,这个平台不支持可写的静态数据,而静态数据经常被用于在游戏中来最优化访问广泛使用的数据。
智能电话还有相对限制的硬件。 显示屏幕有有限的分辨度、尺寸和彩色深度。 小键盘只有有限数目的按键,并且按键的布局可能不便于玩游戏。 在不同硬件解决方案之间,键盘的布局可能会有很大的不同,因此游戏应该提供用户重新定义按键的功能。 智能电话还没有足够的端口来支持各种游戏控制器,像方向盘和游戏杆,这些都是在PC环境中很常见的。 所有这些限制决定了哪些游戏可以被实现并且安装到一部智能电话上,而不会降低它们的可玩性。 然而从长远的观点来看,智能电话中使用的技术将越来越精巧,新的特性和解决方案将被引入。
内存
在内存有限制的设备中,内存管理处于一种非常重要的地位。 这关系到运行时间内存使用和最后的编译代码长度。 大部分基于Symbian OS和Series 60的设备只有8 MB内存甚至更少。 除了内存以外,这种设备只有用于预先安装的软件和用户数据区域的只读存储器,只读存储器用于安装应用程序和系统的可写入数据和持久数据文件。 此外,便携式存储卡,例如小型闪存( CF)卡或者多媒体卡( MMC),可能依靠硬件支持。
内存使用最重要的规则是所有分配的存储器都应该在在尽可能的早期释放。 Symbian OS仿真程序提供一个宏用于内存检验,默认情况下所有应用程序都有图形用户界面。 如果这个宏不能释放内存,这个宏将使应用程序变混乱,这样就会出现内存泄漏。 在一个目标硬件中,OS的核心记录每个线程的内存并且在线程退出的时候自动的释放它。 这保证所有的内存都在应用程序退出的时候被释放。 运行时间一长,应用程序和服务器可能出现一个问题。 它们在结束使用它们的时候,如果它们不释放已经不需要的资源,那么就会有很多资源被系统保留起来而不能被应用程序所使用。
当实现一个应用程序时,堆栈存储器的使用值得注意。 在Symbian OS中,每个线程都有自己的存储堆栈,不能在线程已经启动以后增大。 Series 60中的应用程序的默认堆栈尺寸只有20kB,所以它应该谨慎使用。 仿真程序环境和可用的堆栈空间中的目标硬件之间还有分歧。 仿真程序中的堆栈尺寸不像在硬件中那样受限制,因为取而代之的是使用Windows自己的堆栈。 这就是为什么所有的软件都应该在开发早期在硬件上测试,并且使用它们最大尺寸的堆栈变量。 大多数的堆栈都会因为使用堆栈描述符而溢出。 这可以通过从堆栈分配描述符并且通过使用自动对象来避免。 还有,递归的使用可能是一个非常消耗堆栈的事情。 如果真的无法避免递归程序设计,那么传递的参数和递归内部的局部自动变量也应该尽量最小化。
为了最小化编译代码的大小,应该遵循下面的方针:
1. 除非有必要,否则不要导出方法。
2. 不创建不必要的虚拟方法。
3. 不过度使用TRAP宏。
4. 避免重复代码。
5. 找到可分解的函数
6. 使用公共的控件和组件
为了能够从一个动态链接库外访问函数或者数据,被导出的方法被列在一个动态链接库导出表中。 虽然在Symbian OS中,这些方法是以序号而不是以名字来导出的,所有的不必要导出的方法都会导致导出表的大小增大。 这是为什么方法应该只在它们有目的的用于它们被引入的库之外的时候,它们才被导出。 这同样应用到记录在动态链接库中的虚拟函数表中的虚拟方法。
TRAP宏的使用应该小心地设计。 这不意味着它们会过度使用,因为它们的负面影响是增大编译的代码的尺寸。 Symbian OS的应用框架提供的TRAP经常对于应用开发者来说已经足够了,他们不需要编码他们自己的TRAP。
在列表中的最后三个条目是最常用的方法,以最小化所有平台上的代码尺寸,并且不需要被详细的讨论。
由于游戏的图形特性,位图经常占用大量的内存使用量。 这应用于随机存取存储器和用户数据区域使用量。 有助于减少使用量,而不减少位图的数目的最有效的方法是减少它们的颜色数。 Symbian OS支持16777216种颜色的24位位图,但是实际的最大颜色数受目标硬件限制。 这就是为什么所有的位图都不应该转化为比硬件设置最大颜色数更高。 小型的低细节的不需要许多颜色的位图,应该参考上的规则转化为比最大颜色数要低的颜色数。 例如8位颜色适合于大多数的子图形。 而所有的mask都应该被转化为1位的位图。
计时器
对于大多数游戏来说,计时是必不可少的。 系统端提供的计时服务用于各种目的。 在更复杂的游戏中,游戏的场景经常更新,并且用户的输入在一秒中被读很多次。 在更简单的游戏中,计时被用于处理游戏玩家的回合或者评估一个游戏玩家是否成功的解决一个给定的问题等等。 大多数的游戏都需要来自系统的计时支持。
在Symbian OS中,最不足的服务之一就是计时服务。 OS不支持低级计时中断,它只提供一个有最大频率为64 Hz的核心端计时器。 相同的时钟频率还被用于线程的循环调度。 仿真程序环境中,最大时钟频率是10 Hz,这会导致游戏的测试出错。 系统的最大时钟频率可以使用UserHal::TickPeriod方法访问,是用一种平台无关性的方法来给出时钟周期。 这个方法在e32hal.h头文件中被引用。 图3.1是Symbian OS计时器类的类图。
图3.1 Symbian OS计时服务的类图
核心端计时器可以使用RTimer类取得,这是一个到系统端服务器的操作。 它提供一个简单的应用编程接口来请求三个不同的计时事件:一段给定时间之后的事件,给定时间的事件和一个完成一秒钟的给定分段的事件。 这个应用程序编程接口需要TRequestStatus作为一个参数被传送进去,交付给应用程序开发者来使用活动对象作为事件处理程序。 为了简化RTimer的使用,Symbian OS提供一个抽象活动对象CTimer,封装了RTimer的使用。 这可以使用一个简单的封装完成,应用程序开发者需要从CTimer中派生出这个类,然后覆盖当一个请求完成时被调用的RunL方法。 然而,由于在计时服务中使用了活动的对象,真实计时事件处理可能延迟。 当一个计时器请求完成的时候,另一个活动的对象可能已经被执行,并且正在处理这个记时事件的活动的对象直到另一个活动的对象结束它的RunL的时候它才会被列入计划。 这虽然不可避免,但是对于计时精度的影响可以通过让所有的RunL方法尽可能短时间运行来达到最小化。 如果另一个带有更高优先权的活动对象被列在前面的话,这个事件处理可能还是会被延迟。 这可以通过使处理计时事件的活动对象比其它活动对象具有更高的优先权来避免。
Symbian OS还提供两个CTimer导出类来重复的进行计时事件:CPeriodic和CHeartbeat。这两个类都在一个事件发生时调用一个回调方法。 对于CPeriodic,事件的间隔可以以微秒给出,而对于CHeartbeat来说,事件的间隔仅仅以秒划分,这两个类都是被TTimerLockSpec枚举定义的。 最小的分数是十二分之一。 在CPeriodic中,给定的间隔近似到最接近的系统计时分辨率。 CHeartbeat提供了一个方法来使计时器与系统计时器同步。 如果错过了一个或多个记时事件,那么它的回调方法Synchronize将被调用,并且使用这种方法,它可以提供一个应用程序执行必要的恢复动作的可能性。 上面提到的计时类可以从e32std.h和e32base.h头文件中找到。
按键事件处理
Symbian OS是一个事件驱动系统–所有的应用程序和服务器都可以被看作为事件处理程序。 象按键事件这样的事件都是被活动对象操作的,使事件处理非抢先式计划。 一个用户按下一个键的事件流程的示例如图3.2所示。
图3.2按键事件流程
当一个用户按下一个按键时,键盘硬件生成一个中断,被键盘驱动程序捕获。 在分解按键事件之后,驱动程序把它发送到一个称为窗口服务器的系统端线程中。 窗口服务器发送事件到窗口群具有焦点的应用程序中。 这是使用一个控制环境( CONE)来实现的,这是一个介于窗口服务器和用户界面库之间的应用编程接口。 CONE和窗口服务器在第四章中做出了解释。
在应用程序端中,按键事件是在窗口服务器调用的OfferKeyEventL方法处理的。 每按下一个键就会生成三个单独的事件。 第一个事件是EEventKeyDown,这是当一个键被按下的时候生成的。 然后是EEventKey,当键被松开的时候生成了EEventKeyUp事件。 这些事件类型是被TEventCode枚举定义的,被传送到OfferKeyEventL作为第二个参数。 第一个参数是一个结构(struct)TKeyEvent,指定关于事件的更详细的信息。 如果一个键按下的时间长于0.8秒,窗口服务器发送另一个EEventKey事件到应用程序,长时间按键事件。 如果这个键按下的时间比这个还长,那么窗口服务器每隔0.25秒钟发送一次按键重复事件。 这些时间帧对于Series 60来说是缺省值,它们可以被应用程序修改。
TKeyEvent有一个成员变量iRepeats,用于从按键重复事件中分离出长时间按键事件。 一旦这个变量不为零,应用程序必须知道当最后一次按键事件被接收的时候这个值为多少。 如果最后的事件的iRepeats等于零,那么一个长时间按键事件就被接收,而如果不为零的话,就接收一个按键重复事件。 iRepeats变量是一个32位带符号整数,定义了自从最有一次处理的事件之后定义的事件数。 因为大部分的案件事件在某个地方被处理,所以这个变量不定义自从第一个按键事件之后的实际重复数。 如果它们想知道键到底被按下去多长时间,这就是为什么应用程序需要它们自己计算重复的次数。 TKeyEvent和TEventCode的定义可以在w32std.h头文件中找到。
经常需要按键事件的游戏应该设置它们自己的按键重复率。 按键重复期间可以使用窗口服务器的带有两个参数的SetKeyboardRepeatRate应用编程接口来更改。 第一个参数在第一次按键重复事件之前指定时间,这相当于一个长时间按键事件,而第二个参数指定后继按键重复事件之间的时间。 设置时间框相等产生了一个线性的重复速度,也就是说第一次按键事件和后续按键事件之间的时间框相等。 因为重复速度是全系统范围的设置,所以它们应该在另一个应用程序进入前台的时候被改回默认值。
默认情况下,在Series 60中大部分的按键被阻塞;仅有电源键和编辑键是非阻塞按键。 无论如何,按键覆盖对于游戏来说是非常重要的,因为游戏中往往需要用户能同时按下两个键。 这就是为什么Series 60提供能用于禁止按键阻塞的应用编程接口。 应用程序用户界面的基本类CAknAppUi提供SetKeyBlockMode方法,可用于禁止按键阻塞。 这个应用编程接口接受一个TAknKeyBlockMode枚举作为一个参数,这个枚举可以有两个可能的值:EDefaultBlockMode和ENoKeyBlock。 按键覆盖也是一个全系统的设置,应该在游戏不在前台的时候被恢复到缺省值。
声音
在Symbian OS中,播放和操作声音是由媒体服务器处理的。 媒体服务器支持各种声音文件格式,比如 wa、au和wve,并且提供一个应用编程接口以便应用程序能够开发补充的文件格式插入式模块。 媒体服务器的客户端应用编程接口被分为三个不同的接口:音频示例编辑器,音频音调播放器和音频示例播放器。 音频示例编辑器接口提供了高级音频操作方法,可以用来录音、编辑和播放声音。 音频音调播放程序接口启动应用程序来创建和播放合成的声音。音频示例播放程序接口可用于播放样本数据文件。 媒体服务器接口的使用需要在同一线程中运行一个活动的调度程序。
对于大多数游戏来说,音频示例播放器接口提供了所有需要的特性来实现要求的音响效果。这个接口由MMdaAudioPlayerCallback和CMdaAudioPlayerUtility类组成。 MMdaAudioPlayerCallback是一个mixin类,提供回调方法来通知客户端类样本的初始化或者播放已经完成。 这就是为什么这个使用样本播放程序接口的类需要从mixin类中继承而来。 CMdaAudioPlayerUtility类提供了加载和播放样本的方法,并且可以设置播放的音量。 这个类可以仅仅和单一样本数据关联,这样一个应用程序有多少个不同的样本数据文件,它就需要创建多少CMdaAudioPlayerUtility的实例。 下面的代码是使用的CMdaAudioPlayerUtility类的示例。
∥创建一个帮本播放程序并且从一个文件装入样本 CMdaAudioPlayerUtility* samplePlayer = CMdaAudioPlayerUtility::NewFilePlayerL( KSampleFileName, *this ); ∥播放样本 samplePlayer->Play(); |
在Series 60中,每个应用程序对于每个键都有一个默认的声音。 这个声音还可以取决于这个按键事件是否是短时间、长时间或者重复按键。 Series 60应用程序用户界面类CAknAppUi提供对应用程序在资源文件中指定它们自己的按键声音效果的支持。
RESOURCE AVKON_SKEY_LIST r_example_skey_list { list = { AVKON_SKEY_INFO { key=EStdKeyLeftArrow; sid=EAvkonSIDNoSound;}, AVKON_SKEY_INFO { key=EStdKeyLeftArrow; sid=EAvkonSIDNoSound; type=ESKeyTypeLong;}, AVKON_SKEY_INFO { key=EStdKeyLeftArrow; sid=EAvkonSIDNoSound; type=ESKeyTypeRepeat;} }; } |
可用的声音标示符SIDs在avkon.hrh头文件中指定。 在游戏中,如果一个键被长时间按下,重复声音应该通过指定这个按键事件的声音标识符为EAvkonSIDNoSound来禁止。 这是因为每当一个按键重复事件接收的时候播放重复声音会消费大量处理时间。 如果一个游戏需要连续的声音,应该使用音频样本播放器来代替。
安装
在Symbian OS中,应用程序的安装是使用安装文件sis文件来完成的。 Sis文件包含要被安装的文件以及安装时需要的信息。 sis文件中的数据被压缩以节省存储器,并且最小化sis文件传送到终端的时间。 应用程序的安装可以直接从一台安装了Series 60 PC Suite的个人计算机上运行相应的sis文件来完成。 Sis文件还可以通过首先使用各种通信技术例如WAP、蓝牙和Infrared Data Association(红外线数据协会)来下载文件,然后在一个通信应用程序中打开它。
Sis文件使用程序包文件pkg文件构造,让必要的信息汇编为一个sis文件:
; MyGame.pkg ; Specifies an installation file for MyGame ;Languages &EN ;Header #{"MyGame"},(0x1000ABCD),1,0,0 ; Required line for Series 60 devices. (Added by NOKIA) (0x101F6F88), 0, 0, 0, {"Series60ProductID"} "\epoc32\release\thumb\urel\MyGame.app"- "!:\system\apps\MyGame\MyGame.app" "\epoc32\release\thumb\urel\MyGame.rsc"- "!:\system\apps\MyGame\MyGame.rsc" |
上面的这几行里,凡是前面带有分号的,都是注解行。 第一个非注解行指定支持的语言变体。 一个sis文件可能包含多于一个的语言变体,虽然每次只能安装一个变体。 第二行专留作一个程序包头,指定应用程序的名称和标识符,主要的和次要的版本号以及构造号。 在此之后,是Series 60 Product Uid。 这个指出这个应用程序可以安装在哪个Series 60平台版本和设备。 可以使用多个Series 60 Product Uid。 下面是大部分公共Series 60 Product Uid:
Nokia 7650 | 0x101F6F87 |
Nokia 3650 | 0x101F7962 |
Nokia N-Gage? Mobile Game Deck | 0x101F8A64 |
SX1 | 0x101F9071 |
Series 60 Platform v0.9 | 0x101F6F88 |
Series 60 Platform v1.0 | 0x101F795F |
下面的几行定义将要被安装的文件。 每行指定PC中的源路径,以及在终端上的目标路径。如果目标驱动器字母被指定为一个感叹号,用户可以在安装时选择驱动器。 程序包文件格式还支持一些可以被使用的可选参数,例如指定和语言有关的文件。 sis文件是使用一个称为makesis的命令行工具汇编的,接收一个相应的pkg文件作为一个参数。
Symbian OS可以被理解为一个面向位图的操作系统。Symbian OS中所有的应用程序都可以使用位图,尤其是游戏程序。虽然使用基本绘图方法(例如DrawLine和DrawEllipse)能够画出更加小型的图形,但是位图能够更有效地画屏,而且输出的图形更加细腻。
Symbian OS有自己的位图文件格式MBM,这是一种多位图文件。 窗口位图使用一个位图转化工具bmconv来创建MBM。 由于一个MBM文件可能包含多个位图,所以bmconv还产生一个位图头文件MBG,这个头文件提供了一个访问位图的ID。当从MBM文件加载一张位图的时候,应该包括相应的头文件,同时应当使用恰当的标识符作为加载位图的方法的参数。 位图可以在项目文件中定义:
START BITMAP [target-file] HEADER TARGETPATH [targetpath] SOURCEPATH [sourcepath] SOURCE [colour-depth] [source-bitmap] END |
bmconv可以创建两种不同类型的Symbian OS位图:只读存储位图和非只读存储位图。非只读存储位图,亦称文件存储位图,使用编码RLE压缩,它们在使用之前需要被装载到RAM中。为了提高描画速度,一般不压缩只读存储位图,并且可以直接从ROM中使用它们。默认情况下是bmconv建立文件存储位图(即非只读存储位图)。
Symbian OS提供对蒙版(mask)的支持。蒙版是黑白位图,白色描画透明区–只有蒙版中的黑色区域上的像素是从原始位图上根据想要的图形描画来的。因为蒙版只需要两种颜色,所以它们应该被转化为1bit的位图以节省存储空间。图1说明了使用蒙版的示例。
图1 使用蒙版来描画透明位图的示例
Series 60提供了一个名为makemask的命令行工具,用来从8 bit位图中创建1 bit的蒙版。 makemask使用原位图中的最后一个调色板索引作为透明色。
虽然Symbian OS提供了设置位图调色板的应用程序编程接口,但是这些接口并没有实现。一旦补充这些应用程序编程接口,系统就支持彩色显示。当添加支持的时候,Symbian OS判定只支持Netscape调色板色彩立方校正。 Series 60提供它自己的8 bit位图调色板。Bmvconv被修改来把8 bit位图转换为使用Series 60调色板(提供216种颜色和10种灰色调)。 这能防止第三方开发者使用它们自己的调色板,因为使用它们自己的调色板经常会对位图输出造成影响。 尤其是需要一种颜色的多种色调的位图,例如创建一个颜色渐变位图,至少应该转化为12位的位图。 Series 60调色板定义在thirdpartybitmap.pal调色板文件中。
位图的管理由CFbsBitmap类来完成,这个类提供了用于创建和装载位图的方法,并且定义它们的色深度和尺寸。它使用RFbsSession类来访问FBS,因此用户是接触不到会话类的。 CFbsBitmap还提供了直接访问位图图像数据的方法。 可以使用DataAddress方法获得一个指向数据地址的指针,并且可以使用GetScanLine方法访问一条指定的扫描线。
根据位图的尺寸把位图分解到FBS中的两个不同的堆里。小于4kB的位图被保存到一个堆里,大于4kB的位图保存到另一个堆里。进行这种分解是为了防止存储碎片,存储碎片是大的位图被创建和销毁的时候经常出现的–大位图的堆能自动整理碎片。由于碎片整理的原因,当操作大位图的内容时,堆需要被锁定。为了防止碎片整理和操作同步进行,TBitmapUtil类提供了锁定堆和解锁堆的操作。只有在一个位图的图像数据被直接编辑的时候才需要锁定堆–描画和复制方法提供了自动锁定功能。 下面的示例在位图是大的位图并且使用颜色填充位图的情况下锁定FBS。它假定位图的每个象素都使用16位。这适用于12位和16位位图。
// Lock the heap if a large bitmap if ( bitmap->IsLargeBitmap() ) { TBitmapUtil bitmapUtil( bitmap ); bitmapUtil.Begin( TPoint(0,0) ); } // Edit bitmap TSize bitmapSize = bitmap->SizeInPixels(); TUint16* bitmapData = (TUint16*)bitmap->DataAddress(); TUint16 colour = 0; for ( TInt y = 0; y < bitmapSize.iHeight; y++ ); { for ( TInt x = 0; x < bitmapSize.iWidth; x++ ) { *bitmapData++ = colour++; } } // Release the heap if ( bitmap->IsLargeBitmap() ) { BitmapUtil.End(); } |
为了使描画位图比使用CfbsBitmaps还快,窗口服务器提供它自己的位图类CWsBitmap。它通过取得位图处理的所有权来除去窗口服务器和FBS之间多余的上下文转换。CWsBitmap从CFbsBitmap继承而来,并且实现了其所有相同的方法。如果你在开发应用程序的时候很重视描画速度,那么你应该使用CWsBitmap来替代它的基类。
应用程序在屏幕上的描画一般是使用CWsScreenDevice图形设备来完成,与CWindowGc图形上下文相关联。CONE提供了一个CWindowGc实例作为描画控件的标准图形上下文。它被CCoeEnv创建并且可以使用CCoeControls::SystemGc方法访问。CWindowGc的描画方法在客户端窗口服务器缓冲区上进行缓冲。
描画要么是一个系统初始事务要么是一个应用程序初始事务。系统初始描画在窗口创建的时候被触发,或者当窗口内容因为窗口重叠而失效的时候被触发。对于后一种情况,窗口服务器为每个窗口保持一个无效的区域。如果一个窗口需要重画,窗口服务器发送一个重画事件到拥有无效窗口的应用程序中。CONE然后使用无效区域来建立需要被重画的控件,并且调用它们的Draw方法。这就是为什么每个控件都应该实现Draw方法来重画它们自己。CCoeControl中的Draw的默认为控件为空。下面的代码说明了Draw方法的示例:
void CExampleControl::Draw( const TRect& /*aRect*/ )
const { // Get the system graphics context CWindowGc& gc = SystemGc(); // Set drawing settings gc.SetBrushStyle( CGraphicsContext::ESolidBrush ); gc.SetBrushColor( KRgbRed ); // Draw gc.DrawLine( TPoint(10,10), TPoint(30,10) ); }
Draw方法的TRect参数指明了需要重画的无效区域。然而大多数控件忽略矩形,由于它非常简单并且重新描画整个控件也不是非常慢。 当一个应用程序的数据或者状态改变的时候,需要应用程序初始化描画,并且屏幕需要更新。CCoeControl提供非虚拟DrawNow方法,指明控件将要描画的窗口服务器,调用控件的Draw方法,最后指明完成描画的窗口服务器。CCoeControl还提供了DrawDeferred方法,使窗口无效并且在窗口服务器中触发一个新的重画事件。这两个方法之间的差异是DrawNow强制控件立即重画自己,而DrawDeferred导致一个重画事件将使用低优先级操作。由于CONE使用比重画事件更优先的级别处理用户输入事件,所以任何未定的用户输入事件都将首先处理。但由于需要重画整个控件,故都是很繁重的操作,通常只有改变的部分需要重画,这可以使用下面的代码做到:
void CExampleControl::DrawBitmap( const TPoint& aPoint, const CFbsBitmap* aBitmap )
{ // Get the system graphics context and control rectangle
CWindowGc& gc = SystemGc(); // Establish drawing rectangle TRect rect = TRect( aPoint, TSize( aBitmap.iWidth, aBitmap.iHeight ) ); // Activate graphics context ActivateGc(); // Invalidate window Window().Invalidate( rect );
Window().BeginRedraw( rect ); // Draw a bitmap
gc.DrawBitmap( aPoint, aBitmap );
Window().EndRedraw(); // Deactivate graphics context
DeactivateGc();
}
上面的示例代码在aPoint参数定义的位置画一个CFbsBitmap。示例中值得注意的是图形上下文在使用之前需要激活,在描画完成之后失活。还有窗口服务器需要取得客户端即将启动重画的信息,这使用BeginRedraw方法来完成。由于窗口服务器只允许一个应用程序在无效区域中描画,所以需要Invalidate方法。在一个系统初始重画中,CONE激活图形上下文并且调用用于应用程序的BeginRedraw方法。如果窗口已经无效了,那么Invalidate方法就不必被调用了–这就是为什么系统初始需要被首先描画。
子图形(精灵)
子图形是一个经过蒙板化(Mask)的位图,可以在应用程序不重画底层窗口的情况下移动。如果游戏不需要经常更新背景,那么使用子图形就再好不过了。例如类似于PacMan这样的游戏,在这种游戏中动画在一个不能卷轴并且固定的背景上移动。重画是靠窗口服务器来执行的,替代一个较高优先性的任务。这种游戏要考虑的是平滑的动画和子图形的运动。Symbian OS提供两种不同的子图形:指针和动画位图。图1说明子图形类的层次。
图1说明子图形类的层次
RWsSpriteBase是一个用于子图形的抽象基本类。它拥有一个或多个包含子图形的位图数据的TSpriteMembers。通过指定带有不同的位图的多个成员,子图形就可以活动起来了。TSpriteMember还定义了位图的蒙板,子图形中位图的位置和位图显示的时间间隔。RWsSprite是一个用于子图形的具体的类。除了构造器之外,它只提供一个方法SetPosition,可用于移动子图形。下面的代码说明了使用从MBM文件中装载的位图创建子图形的示例。
RWsSprite sprite = RWsSprite( iEikonEnv->WsSession() );
User::LeaveIfError( sprite.Construct( Window(), TPoint(0,0), 0 );
for ( TInt i=0; i < 8; i += 2 )
{
iMember[i/2].iBitmap = new ( ELeave ) CFbsBitmap();
User::LeaveIfError( member.iBitmap->Load( KBitmapFile, i, EFalse ) );
iMember[i/2].iBitmap = new ( ELeave ) CFbsBitmap();
User::LeaveIfError( member.iMaskBitmap->Load( KBitmapFile, i+1, EFalse ) );
iMember[i/2].iInvertMask = EFalse;
iMember[i/2].iOffset = TPoint(0,0);
iMember[i/2].iInterval = TTimeIntervalMicrosecond32(100000);
User::LeaveIfError( sprite.AppendMember( iMember[i/2] ) );
}
在子图形成员已经更新并且附加到RWsSprite类之后,子图形可以通过调用RWsSpriteBase::Activate来激活。在此之后,这个子图形显示在屏幕上,并且准备移动。子图形的内容可以使用RWsSpriteBase:UpdateMember方法来变化。因为CFbsBitmaps还可访问窗口服务器,所以只有子图形的位图句柄被发送到窗口服务器。这使子图形的位图的更新相当迅速。当子图形不再需要的时候,窗口服务器需要调用RWsSpriteBase::Close来释放资源。但不释放需要被删除的客户端成员数据。RWsPointerCursor是一个用于应用程序创建光标的类。
双缓冲
如果一个游戏的图形由多个需要被经常更新的运动对象组成,窗口服务器的客户端缓冲可能被充满并且可能会在所有对象都更新的时候溢出。用户可能会发现屏幕出现闪烁。如果一个视图仍然在更新的时候,可能会出现闪烁或者其他不希望的效果。这些问题的解决方案是双缓冲,图形先被画在一个屏外位图上,然后被画到屏幕上作为一个单一窗口服务器操作。尤其是对于那种在一秒钟内重画几次屏幕的游戏,使用屏外位图可以改善它们的性能。
一个屏外位图可以使用位图化的图形上下文和图形设备类来创建:CFbsBitGc和CFbsBitmapDevice。它们使用其他的上下文和设备类来创建和使用。为了获得额外的性能,位图自己就应该是一个CWsBitmap位图。在屏外位图更新之后,它可以使用正常的窗口服务器的描画方法画在窗口中。
当一个应用程序在一个窗口画位图时,它转化为和窗口相同的显示模式。这是一个很消耗时间的操作,实质上可能降低描画的速度。因此把位图用于动画的游戏应该在动画开始之前就完成转化。转化可以通过使用一个屏外位图来执行,如下面的示例方法演示:
CFbsBitmap* CExampleControl::LoadAndConvertBitmapL(
Const TDesC& aFileName, TInt aBitmapId )
{
// Load the bitmap
CFbsBitmap* originalBitmap = new ( ELeave ) CFbsBitmap();
CleanupStack::PushL( originalBitmap );
User::LeaveIfError( originalBitmap->Load( aFileName, aBitmapId, EFalse ) );
// Create a new bitmap, graphics device and context
CFbsBitmap* newBitmap = new ( ELeave ) CFbsBitmap();
CleanupStack::PushL( newBitmap );
newBitmap->Create( originalBitmap->SizeInPixels(), Window()->DisplayMode() );
CFbsBitmapDevice* graphicsDevice = CFbsBitmapDevice::NewL(bitmapConverted );
CleanupStack::PushL( graphicsDevice );
CFbsBitGc* graphicsContext;
User::LeaveIfError( graphicsDevice->CreateContext( graphicsContext ) );
TPoint zero(0,0);
// Blit the loaded bitmap to the new bitmap
bitmapContext->BitBlt( zero, originalBitmap );
CleanupStack::Pop(3);
delete bitmapContext;
delete bitmapDevice;
delete originalBitmap;
return newBitmap;
}
示例方法使用一个文件名和位图ID作为参数,并且从一个MBM文件中装载相应的位图。如果一个游戏有许多位图应该转化,那么应该在游戏或者等级的初始化阶段转化。因此用户就不会看到这个操作了。
直接描画
使用窗口服务器在屏幕上描画需要一个上下文转换,这会减慢描画速度。为了绕过窗口服务器省去繁琐的上下文转换,可以直接访问屏幕。这被称作直接描画。在Symbian OS中有两种方法来直接在屏幕上描画。
CFbsScreenDevice是一个可以被发送到屏幕驱动程序SCDV.DLL的图形设备。在创建一个CFbsBitGc图形上下文之后,它能像任何其他的图形设备一样使用。然而,可以直接在屏幕上描画,而不需要使用窗口服务器。直接在屏幕上描画的另一种方法是从系统中查询屏幕内存地址。这可以使用UserSrv类来实现:
TPckgBuf<TScreenInfoV01> infoPckg;
TScreenInfoV01& screenInfo = infoPckg();
UserSvr::ScreenInfo(infoPckg);
TUint16* screenMemory = screenInfo.iScreenAddress + 16;
屏幕内存有一个32字节的头。
即使在屏幕内存内写数据比CFbsScreenDevice稍微快一点,但是功能可能根据硬件和屏幕的设备驱动程序的不同而有差异。在一些基于Symbian OS的终端中,屏幕在内存变化的时候自动从屏幕内存中更新,而在其他的终端中描画需要明确的激活。屏幕内存地址只对目标硬件有效,因此描画代码需要分为硬件和模拟器两部分。在模拟器环境中,可以描画到一个屏外位图中,而不是屏幕内存中,然后使用正常的窗口服务器描画方法位块传送到屏幕上。环境可以通过使用__WINS__定义来检测出来。
#ifdef __WINS__ // Emulator environment
// Draw to an off-screen bitmap
#else // Hardware environment
// Draw directly to the screen memory
#endif
这两种直接描画方法的一个共同的问题是窗口服务器不了解描画,因此它不能通知应用程序是否出现另一个窗口或者窗口组。 即使当应用程序失去焦点的时候得到一个事件,它们也不能停止直接描画,因为直接描画实在太快了,并且屏幕内容有可能被弄乱。 这可能发生在玩游戏的时候,突然有电话打进来的情况下。
新近的GT 6.1版本提供了一个应用编程接口用于直接描画,将能解决前面提到的问题。 这个应用编程接口由两个类组成:一个MDirectScreenAccess类,提供用于应用程序的回调方法,还有一个CDirectScreenAccess类处理与窗口服务器的通讯。 下面的代码说明CDirectScreenAccess实例是如何构造的,以及直接描画支持是如何激活的。
iDrawer = CDirectScreenAccess::NewL(iEikonEnv->WsSession(), *iEikonEnv->ScreenDevice(), Window(), *this);
iEikonEnv->WsSession().Flush();
iDrawer->StartL();
iDrawer->ScreenDevice()->SetAutoUpdate(ETrue);
CDirectScreenAccess的NewL方法获得一个窗口服务器会话、CONE的图形设备、应用程序窗口和一个到MDirectedScreenAccess导出类的指针作为参数。 在CDirectScreenAccess::StartL被调用来激活直接描画支持之前,客户端窗口服务器缓冲应该溢出。 为了能自动更新屏幕,屏幕设备的SetAutoUpdate方法需要使用ETrue参数。 当直接描画支持激活的时候,CDirectScreenAccess产生一个CFbsBitGc图形上下文,可以被应用程序用来在屏幕上绘画。
iDrawer->Gc()->BitBlt( TPoint(0,0), iBitmap );
当另一个窗口出现在应用程序窗口上时,CDirectScreenAccess从窗口服务器取得一个事件来中断描画。 CDirectScreenAccess然后调用MDirectScreenAccess派生类的AbortNow方法,这个方法必须被应用程序重载以便中断描画。 为了防止屏幕被弄乱,窗口服务器直到中断描画事件被处理的时候才画重叠窗口。
在这一讲中中,将讨论Symbian OS的通讯组件。必须强调的是,从一个游戏开发者的角度来看,通讯技术是非常重要的。 最后介绍如何接收补充的游戏数据。
通讯体系结构
智能电话的移动特性和通讯技术的飞跃发展决定了它们对通讯模块的需求。 全世界都在使用智能电话,但是每个国家或者地区可用的通讯服务可能会有很大的差异。 现有的服务和技术不断地发展,而且新的技术层出不穷。 这些事实就要求智能电话通讯模块具有灵活性和扩展性,因此Symbian OS的通讯体系结构是基于这些思想来设计的。 它由几个小的模块组成,并且支持可以在运行期间装载的可插入式模块。 而且通讯设置可以在系统不重新启动的情况下就进行更改。
Symbian OS 6.1版本支持多种通讯技术,图1中都作了介绍。 特定的智能电话的可用的技术取决于它的硬件解决方案。 例如诺基亚7650或者诺基亚3650没有串行的数据线,但是可以通过蓝牙技术支持串行通讯。
图1 Series 60通讯技术
Symbian OS的通讯体系结构基于三个通讯服务器:ETEL、C32和ESOCK。 服务器提供的通讯服务是异步操作,因此它们需要被封装到活动对象里。一个客户应用程序一般产生三个不同的活动对象:一个用于传送数据、一个用于接收数据还有一个用于应用程序端通讯引擎。通讯模块的关系如图2所示。
图2 Symbian OS的通讯组件
ETEL是一个电话通讯服务器,用于应用程序访问各种电话硬件和服务,例如GSM手机、模拟调制解调器和传真通讯服务。 这个服务器使用可动态装载的插入式模块–电话通讯服务器模块(TSYs),把硬件特定的信息转化为应用程序可理解的格式。 这个服务器的客户端应用编程接口定义在etel.h头文件中,它主要由RTelServer、RPhone、RLine和RCall类组成。
串行通讯服务器
串行通讯服务器(C32)为它的客户端提供一个串行端口应用编程接口。这个服务器使用通讯服务器插入式模块(CSYs)处理实际的通讯协议。Symbian OS提供多个CSY模块,例如处理RS232和红外线串行通讯。应用程序开发者还可以使用串行协议模块应用编程接口来开发自定义CSY模块,自定义CSY模块定义在cs_port.h头文件中。
串行通讯服务器使用方法都是很相似的,不管是否使用了CSY模块。首先在初始化阶段,客户端加载所需要的驱动程序,打开服务器并且装载CSY模块。在实际的设备已经打开并且配置之后,服务器准备发送并接收数据。最后,需要释放所有的资源。 串行通讯服务器的客户端应用编程接口定义在c32comm.h头文件中,主要由RComm和RCommServ类组成。 下面的代码是一个红外线串行通讯初始化阶段的示例。
// Load device drivers TInt err = User::LoadPhysicalDevice( _L("EUART1") ); if ( err != KErrNone && err != KErrAlreadyExists ) User::Leave( err ); err = User::LoadLogicalDevice( _L("ECOMM") ); if ( err != KErrNone && err != KErrAlreadyExists ) User::Leave(err); // Start serial communications server of type RCommServ User::LeaveIfError( iServer.Connect() ); // Load CSY module for IrComm User::LeaveIfError( iServer.LoadCommModule( _L("IRCOMM") ) ); // Open port of type RComm User::LeaveIfError( iPort.Open( iServer, _L(""IRCOMM::0""),ECommExclusive ) ); |
上面的示例代码可以通过加载ECUART CSY模块来从IRCOMM模式切换到RS232,同时使用COMM:0端口代替IRCOMM:0。 RComm::Open中的ECommExclusive枚举防止其他的RComm客户端使用这个端口。
可以使用RComm:Write和RComm::Read方法向端口写数据和从端口读取数据,它们都是使用一个TRequestStatus和一个描述符作为参数。 当传递完成时,生成一个事件作为活动对象,这个事件的TRequestStatus被传送到传递方法,这就使活动对象的RunL被调用。描述符参数通常是一个8位描述符,因此如果被传送的数据是文本的话,这个描述符需要被转换为Unicode。 数据传送方法可能还使用其它的一些参数,指定数据传送的最大长度或者时间等特征。
对于应用程序来说,使用红外线进行通讯是一个很简易的方法,但是支持IrComm的主要原因便于以前的应用程序的转化,使之可以更容易地使用串行端口来进行通讯。和其它IrDA协议相比,IrComm不利的一面是它不提供所有的IrDA服务。 例如,使用IrComm的最大数据传送速度只有9600比特/秒,这显然对于众多传送大量数据的终端游戏来说是不够快的。
套接字服务器
套接字服务器(ESOCK)提供一个使用套接字的通讯协议的接口。 对于所有的协议和指定协议行为,客户端应用编程接口都是相同的。 套接字服务器使用TCP/IP、IrDA和蓝牙这些协议模块,它们可在运行期间动态装载。 一个协议模块可能包含多个协议。 例如IrDA模块可能包含原始的IrMUX、IrTinyTP、IrLAP、IrLMP和IrObex协议。 对应于TSY和CSY模块的公共协议模块可以由应用程序开发者开发。
套接字服务器的客户端应用编程接口的主要的类是RSocketServ和RSocket。 可以看到它们与RCommServ和Rcomm很相像。 RSocketServ处理一个服务器的会话,提供可用的协议的信息,但是不提供任何数据传送服务–它们由套接字类RSocket提供。 因为客户端应用编程接口对于所有协议都是相同的,某种单一协议的属性和语义与TProtocolDesc结构是有区别的。RSocketServer::GetProtocolInfo方法可用于读取当前装载的协议的信息。 套接字服务器至少需要两个不同的套接字。 一个用于监听进入连接请求,另一个用于建立一个连接和传送数据。 套接字服务器的客户端应用编程接口定义在es_sock.h头文件中。
最吸引游戏开发者眼球的套接字服务器协议是蓝牙技术。 它提供了一个相对快速的、近程的解决方案,并且是免费使用的,因此很适合用于开发游戏。 与红外线通讯相比,蓝牙的优势是它有更大的活动半径以及在终端之间不需要任何可见的连接。
如图3所示蓝牙由一个协议组组成。 Symbian OS 6.1版支持蓝牙1.0版,并且提供给应用程序完全访问RFCOMM、L2CAP和SDP协议的权限。 RFCOMM协议模拟串行通讯,因此简化了从原有应用程序的转化为使用蓝牙技术的过程。 应用程序的通常选择是使用逻辑链路控制和适应协议(L2CAP),提供给应用程序更加强大的函数来控制蓝牙连接。 服务发现协议(SDP)允许应用程序查询服务和服务提供商。通常当创建一个新的连接的时候,SDP搜索要求的终端并且建立连接设置。 Symbian OS还提供一个完整的用户界面组件来搜索可用的蓝牙终端。 它使用Symbian OS的通知框架,在应用程序窗口上产生一个对话类型组件并且把可用的终端作为一个列表来显示。
图3 蓝牙堆栈
游戏数据接收
为了支持游戏接收额外的游戏数据,Series 60能让第三方游戏使用MIME类型在OS中注册它们的数据文件格式。 MIME类型被像WML浏览器和消息应用程序这样的通讯应用程序来使用,用于弄清楚一个具有某种特定类型的文件应该被保存在什么路径。 例如这个文件可以包含用于一个游戏的新的等级、武器或者图形。
在Series 60中,用于游戏的MIME类型是下面这种格式:application/x-NokiaGameData-<APPLICATION-ID>,这其中的<APPLICATION-ID>是Symbian OS游戏应用程序的UID的最后八个数字。MIME类型在Symbian OS应用程序信息文档aif文件中声明:
RESOURCE AIF_DATA { app_uid=0x12345678; // Application UID datatype_list = { DATATYPE { priority = EDataTypePriorityHigh; type = "application/x-NokiaGame-Data-12345678"; } }; } |
DATATYPE结构中的优先级指定当前的应用程序是如何处理这些数据格式。 EDataTypePriorityHigh应用于不能被其它应用程序处理的数据格式。
接收数据的目的路径在名为<APPLICATION-ID>.ini的ini文件中指定。 这个文件需要使用Unicode格式,并且它包含SDDataDir=<GAME-PATH>,在这里<GAME-PATH>是用于接收文件的路径。 这个路径是相对于默认游戏数据目录c:\nokia\games的。 当一个游戏被安装时,ini文件需要被复制到\System\SharedData目录下,它可以通过在一个游戏的pkg文件中指定路径来实现。
Series 60指定一个用于游戏数据文件的遵循MIME类型的标准头结构。 头格式的结构在图4中说明。 Data类型字段可用于指定游戏内部的文件类型。 Name字串是一个Unicode字串,可用于指定一个用户可见的文本来显示菜单中的数据选项。 Data ID和Data版本是用于指定文件数据的类型和版本的号码。 NGDX字段必须包含ASCII字串"NGDX"。